阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

有状态自动扩展系统的设计模式提议

作者:Rogerio Robetti

  • 2023-03-28
    北京
  • 本文字数:8342 字

    阅读完需:约 27 分钟

有状态自动扩展系统的设计模式提议

介绍


面对软件工程界对隔离的需求趋势以及日益增长的可扩展性需求,自动扩展的有状态系统(多指数据库)逐渐复杂化,甚至有时可行性也会成为挑战。因此,多数公司为了让此类系统能适应最高的负载需求而选择了过度配置,但这也带来了另一个问题,过度的资源配置所需求的成本非常之高且无法保证系统的可靠性,激增的需求数量或 DOS 攻击都能轻易击溃系统负载的承受能力。本文中将会更深入地挖掘自动扩展有状态系统时所面临的挑战,并为解决这些挑战给出一个结合已有方法与新手段的设计方案提议。

回顾


回顾软件工程的发展史,我们能清楚看到软件的构建与用户的限制和期望方面的几个重要里程碑。从大型机与大型服务器的集中式方法开始,过渡到桌面应用程序阶段中客户-服务器形式的出现,再到网络应用变革的诸多阶段,从大型单体应用到现代微服务的发展。


纵观历史,隔离的趋势非常明显。其中垂直隔离是指按关注点或上下文将系统分割,通常为数据库与应用程序的隔离,或者用户界面与业务及服务层的隔离。另一种则是水平隔离,即通过增加节点的配置以支持不断增长的需求,这一过程也可以借助 Kubernetes 等工具的帮助进行自动扩展。


这种对隔离的渴求致使了无共享架构等方式的出现,即将应用程序构建为本身不具备状态的形式,也就是我们所了解的无状态应用程序,从而简化扩展的难度。听起来很棒,但工程师们很快便发现很少能有不具备任何状态的,“真正无状态”的应用程序。


退而求其次的方法是将应用程序的一部分(通常为服务或微服务)构建为无状态形式,但即便如此,这部分程序依旧依赖于数据库等有状态的系统以维持状态。这便是本文的主题,我们将讨论这个软件工程师间的共同挑战:如何在现代应用中有效地自动扩展有状态系统?

适用用例


本文不适用于在网页服务器中保持状态的有状态系统。


这种方式为软件工程师们提供了一个可以自行搭建数据库的基础设计方案,切实地解决了单节点上存储系统的问题,并将系统转变为具有自主自动扩展能力的分布式系统。以一个线上商城的微服务架构为例,见图一。


图一:线上商城用例示范


假设出于某些需求的原因,在项目中选择 RocksDB 是相较 Redis 而言更好的键值存储引擎,但麻烦的是,RocksDB 作为存储引擎只能原封不动地部署在单个节点上。假设现在要做另一个对可扩展性需求很高的全球性新系统,那么这篇文章将会是个很好的起点,这里你能找到如何将 RocksDB 这样的单节点存储引擎改造为一个分布式可自动伸缩的应用。


当然,我们这里所说的 RocksDB 只是一个例子,这种设计模式对其他任何的存储引擎或工具均可通用,无论是用于文本索引的 Apache Lucene,还是不具备任何引擎的存内存储,不区分任何的存储引擎、语言、数据类型或结构。此外,本文中所分享的设计也可以为 Mongo、Redis、Postgres 数据库提供自动伸缩的功能。

有状态系统的定义


有状态的系统是指必须处理状态的系统。在现代网页应用中,这项任务通常由数据库承担,不过其他如网页服务器也可以通过将用户会话存储在网页服务器内存中做到。

状态管理在网站中的典型用例是用户的购物车(见图一)。在多个 HTTP 请求之间保留购物车内容,确保在用户结束购物结账付款时,购物车内商品内容和数量的准确无误。这些数据就是被存储在了有状态系统中,而在我们图一的用例中则是 Mongo DB 集群、Redis 集群、Postgres 集群。

自动伸缩:问题所在


我们在面对自动伸缩的有状态系统时,我们往往会思考以下这些问题:什么时候应该开始自动伸缩?有什么契机?应该如何进行伸缩?要如何移动数据?如何保证节点的一致性?以下将会是本文中讨论的重点:

一致性


任何拥有状态的系统都需要保证集群下一个有效状态的一致性。这个领域中相关的研究有很多,比如分别由 MongoDBRedis、和 Postgres 所用的一致性算法 ArkRaft,以及 BDR。这些均是在图一中线上商城示例所用的数据库。


在软件应用中,每当集群必须统一存储记录的下一个值时都会有对一致性的要求,这也是数据库实现中最出名的用例。本文中将涵盖一些能让 Raft 在选择新领导者的一致性上更加智能的建议。

自动伸缩


在有状态系统中,虽然越来越多的产品提供自动伸缩功能,比如云供应商所提供的数据库实例管理,但在实践中,公司内部的类似方案实现似乎总是困难重重,可能存在的原因如下:

  • 缺乏透明度:主流云供应商如 AWS 及 Azure 的自动伸缩功能似乎配置起来很容易,但具体的实现细节却无人知晓。了解该功能的运作原理对拥有大型数据集的关键场景至关重要。数据的迁移时机、策略等等这些问题都应该在产品详情页有明确的说明。

  • 不存在的自动扩展公用模式。自动扩展有状态系统方面至今为止仍无公开可用的模板。

数据迁移的滞后


常见的系统扩展方式是在集群中新增节点,但对于有状态的集群来说,同步其他节点已有的数据是需要时间的,而在某些情况下,这一过程的数据量可能是海量的。


还是以我们的线上商城为例(图一),假设组织的业务涉及多个地理区域,那么数据量将会高达数十亿。在这种规模下,一个清晰且高效的数据同步和迁移方式是非常重要的。

快慢需求的增长


以下这两种情况中,都需要更高的能力:1. 稳定、中长期逐步增加的需求。在先前的线上商城示例中,可以是一段时间内消费者数量的持续增长。2. 无法预测需求量激增,可能带来服务不可用的风险,如系统遭受 DoS 攻击时。

封闭的解决方案


为解决上述这些问题,我们需要更多公开可用的设计模式,盲目相信云供应线的解决方案会奏效不是个好主意。即使云供应商的方案确实有效,我们也很快会发现自己已经和这个特定供应商绑定了,这很不理想。

愿景


作者欲公开提出一种通用且可复用的,客户化自动扩展有状态系统的手段,以最少的配置或运维干预,在单集群内从单节点自动向上(垂直)或向下(水平)扩展至成百上千的节点。本文中所提出的解决方案在现阶段仅仅存在于理论之中,仍需实施和测试。

核心原则

数据类型不可知


这类设计不应局限于任何具体的数据类型,也就是说,同样的解决方案应能处理 JSON 对象、序列化数据、流数据、二进制大对象(Blob),以及其他任何的数据类型。


各司其职,负责写入新状态的集群领导者(leader)只执行写操作,不进行读取;负责读取副本的也只应执行读操作而不进行写入。

让代理成为集群的一部分


有状态集群中必须要有一个不提供读写的代理实现,让集群以这个代理为节点缓慢地同步集群中的数据,并最终在需要时准备提供读或写的请求。

借助平均响应时间触发自动扩展


云供应商所提供的大多数自动扩展方式均是利用了 CPU 和内存阈值,但这种方式并不能带来最佳的用户体验。在某些情况下,终端用户不一定能感知到系统资源的紧张,即使是占用了 99%的 CPU,请求也可能及时交付。


将平均响应时间作为主要的触发条件可以改变扩展的契机,以用户角度看待系统性能。

分片标签的优先级

将每个对象或记录贴上分片 ID 的标签可以规避系统高压时的成本,每当需要分片时可以直接利用已有标签而无需外界干预。

模式的设计


文中所提议的自动扩展有状态系统包含三个不同角色,集群中这些角色各司其职。值得注意的是,作者所提议的设计与系统在单节点的运行能力与生产目的息息相关,这些角色不一定要在自己的进程或节点中运行,但对于测试、POC,甚至是部分 MVP 而言,这点还是很重要的。


话不多说,让我们看看这些角色各自的责任吧。

写入角色(领导者)


写入者或领导者是负责处理写操作的角色,该角色会将新状态写入自身存储并将复制后的数据传给“副本读取”角色。每个分片只有一个写入角色,关于分片的详细解释请见后文。写入角色是一致性的领导者,负责所有的写操作执行,且不承担任何读操作的执行。

副本读取


副本读取角色承担所有的读请求,包含来自写入角色(领导者)的一份复制数据,但如果写入角色和读取角色均在同一节点或进程中运行,则二者可以共享同一存储。在一致性协议选举新的领导者时,副本读取角色负责打开一个与领导者相连的多路通信管道,并在任何一个节点关闭或连接被网络分区中断之前一直保持管道的开放。如此,通过避免连接打开和关闭的开销,有效加快了角色与节点之间的通信速度。

负载管理角色


负载管理角色充当了网关负载平衡器的角色,负责将写请求发送给领导者,将读请求发送给副本读取角色。负载管理角色同时也是一个可接受上千入站请求的背压机制,将目标(对于副本读取或写入者)的并行线程数量限制在可配置的范围内,从而保证了对这些角色的压力控制。这一功能是受阿帕奇的 Tomcat Nio 连接器的启发,此外,该功能也对集群负载激增或 DOS 攻击的防护至关重要,在这类情况下,负载管理角色将会吸收压力,确保读写角色的安全与稳定请求流的接收。


负载管理角色也负责将写请求路由到正确的分片,并在需要聚合时查询请求分发至每个分片中。在将结果返回客户端之前,该角色还会对结果进行聚合及排序,从而减少副本读取角色的工作量。负载管理角色解决问题的方式与数据库代理类似,不同点在于该角色并未以外部插件的形式存在,而是作为集群中的一部分。


负载管理角色可以有一个或多个实例,而每当实例数量到达 CPU 或内存的可配置上限时,则会另外配置一个新的负载管理角色,生成负载管理集群,并在该集群内拥有自己独立的一致性机制。

高层设计


这个设计模式中各个角色之间的基本互动形式可见图二中表示。


图二:解决方案的基本设计

为什么选择 Raft?


作为一款名声在外且饱经考验的一致性校验算法,Raft 远比 Paxos 要简单,但性能却有时能与后者相媲美,正如在论文评论:Raft vs. Paxos中所阐述的。


对于作者所提出的策略而言,Raft 只能同时拥有一个领导者是非常重要的。

智能 Raft


作者提议通过修改 Raft 协议以提升集群的整体性能,让 Raft 能意识到节点之间的不同并选举“更大”的可用节点作为领导者。这点对自动扩展的写操作尤为重要,因为同时只能有一个领导者,那么增强其能力最有效的方式就是提供一个“更大”的领导者,从而触发一个新的选举。Raft 要能识别并选举这个“更大”的节点作为领导者。


另一种方式是通过修改 Raft 使其能接受到一个“切换”指令,从而让集群将领导者切换至这个“更大”的节点。


第二种方式要更好,不仅对协议的修改要更小,还可以将领导者的切换与逻辑解耦。


这里所说的“更大”,是指 CPU、内存、存储技术(SSD),或者其他资源大小,完全取决于有状态集群的需求目的。如果集群需要服务于复杂计算,那么大概会需要“更大”的 CPU 资源,如果集群需要服务于请求,则应该是会需要“更大”的内存资源和更好的存储技术。

自动扩展策略


作者将可扩展性的不同阶段以“马赫”命名,“马赫”本意为超过音速的物体移动速度,而在本文中,各个马赫阶段则喻指集群节点的数量。


注:“马赫”一词仅在本文中使用,并非是行业中专有名词。

可配置的扩展触发器


清楚自动扩展或缩减的时机很重要,在系统承受巨大压力时选择扩展并不是个明智的选择,这也是为什么负载管理角色所提供的背压十分关键。


作者将重点探讨随时间逐渐增加的需求这一情景。为此,有两种基本的配置可用于自动扩展的触发,其中的负载管理角色都需要能够识别触发自动扩展的时机并向操作人员发出通知。操作人员可以为人或软件系统,后者更好。


可配置的触发器如下。

1. 借助平均响应时间的阈值


负载管理角色的任务之一是监测请求的平均响应时间。当请求的平均响应时间到达某个特定的阈值后,则触发扩展的请求。需要向上配置扩展举例:上一个 60 分钟内,每个请求平均响应时间为 3 秒需要向下配置扩展举例:上一个 60 分钟内,每个请求平均响应时间小于 0.5 秒。

2. 借助超时的阈值


在自动扩展信号发送之前,超时的阈值是特定时间段内可能超时的请求所占百分比。需要向上配置扩展举例:上一个 5 分钟内,大于 1%的请求超时需要向下配置扩展举例:不建议使用超时比例作为阈值,出于安全考量,集群中任何级别的超时都不建议用作向下扩展的阈值。


再回到图一的例子中,假设我们目前已经用可自动扩展的 RocksDB 替代了 Redis,这个全新可自动扩展的 RocksDB 集群将无需人类操作者或管理员的干预,自动根据是否超过阈值进行扩展。

在继续阅读本文之前,请注意:- 下文中每个马赫阶段所举的例子大多都注重于提升读取能力,以下文中所述的工作负载隔离,侧面提升写入能力。在马赫 IV 阶段之后会有专门章节表述目标写入能力的扩展。- 本文中的“节点”意指集群中的参与者或运行中的某个进程,不一定代表不同的硬件。- 创建分片之前应先配置需要分配的副本总数。- 任何配置下都可以启动集群,马赫 IV 中所述的是生产所用的最低推荐配置。- 写入、副本读取,以及负载管理这三个角色的实现均为模块化,且常部署在节点之上。举例来说,当某个节点被标记为“写入角色”时,节点虽然仅启用写入角色模块,但也包含副本读取和负载管理的未启用模块,允许该节点在后续按需切换角色。

马赫 I


在集群或系统的初始状态下,所以角色均在同一节点上启用,代表小型用例或测试管道等场景,类似图三中的示例。


图三:单节点部署——马赫 I


在马赫 I 阶段,所有组件均作为单一进程部署在同一个几点之上,这个单一的节点负责管理所有的读写请求。用例:主要推荐用于测试场景,不适合于生产环境。

一致性与副本


在马赫 I 阶段,组件在内存模块内通信,因此不需要一致性或副本。

马赫 II


在马赫 II 阶段,开始部署包含双节点的集群。扩展出的第二个节点一般都是负载管理角色,确保能为响应请求的节点提供背压保护,并允许新节点开始同步数据。

拓扑结构如图四所示。


图四:双节点部署——马赫 II

一致性与副本


在马赫 II 阶段,无需考虑一致性,因为少于三个节点时是不可能建立 Raft 一致性的。


部署在节点二上的副本读取模块会复制与负载管理角色用时运行在节点二上的副本读取角色。需要注意的是,部署了负载管理角色的节点二上的副本读取角色不提供请求,这一设计背后的意图是为时时保持一个“几乎完全”同步的节点,该节点可以以额外的副本读取或领导者节点身份快速加入操作,如作者在马赫 III 阶段所述。


用例:可被用于可靠性不是非常重要或低运营成本的场景中。

马赫 III


在马赫 III 中,集群中又新增一节点,目前一共有三个节点。


新节点永远会以新的负载管理角色进入集群,客户端将被重新定向至新的负载管理,而在马赫 II 中的负载管理角色则会切换为副本读取角色。


马赫 III 的场景可见图五。


图五:马赫 III 中的三节点部署

一致性与复制


三节点时暂时无需考虑一致性,因为即使是拥有三个节点,但只有节点一和二在主动服务于请求。


用例:通过将读写操作分割在不同节点之上,性能已经得到了提升。但如果某个节点故障而没有第二个副本读取节点,那么写入节点将被迫在新节点被分配之前负责处理读取请求。

复原策略


领导者故障:集群回到马赫 II 阶段的拓扑结构,节点二回到处理读写操作的阶段,直到新节点加入集群。节点二(副本读取)故障:领导者或写入角色开始处理读取请求,直到新节点加入集群。节点三(负载处理)故障:节点二将从副本读取角色切换为负载管理,节点一开始执行写入和读取操作。


以上三种情况都会向运营商发送一个信号,提示需要用新节点替换故障节点,新节点永远会以负载管理角色加入集群。

马赫 IV


在这一阶段,集群中有四个节点,其中一个是第二副本读取角色,部署情况如图六所示。


图六:马赫 IV 中的四节点部署


用例:用于生产时工作负载、良好性能,以及故障节点的优秀响应时间所需的最低配置。

一致性与复制


马赫 IV 阶段第一次引入一致性,但此时仍未进行选举。节点一将维持领导者角色,不浪费时间切换至新领导者,专注于写入操作。Raft 实现的扩展对这一安排至关重要。此外,如果领导者故障,则节点二或节点三成为新领导者,并回到马赫 III 的拓扑结构,这一决定由负载管理角色做出。

复原策略


领导者故障:Raft 的协议不允许在集群中只有两个节点时进行选举,因此负载管理会随机在包含运行中副本读取的节点二和节点三之间随机挑选一个领导者。


对马赫 IV 来说,副本与节点管理节点的复原策略与马赫 III 相同,可在创建分片之前将新的副本读取节点添加至集群中以达到可配置的最大数量。

马赫 V、VI……


新的副本读取可不断被添加至集群,直到到达可配置的上线从而触发分片。需要注意的是,副本读取的添加意味着新负载管理角色的添加,从而取代上一个负载管理角色;上一个负载管理会加入 Raft 的一致性并开始服务于读取请求。


用例:随着集群规模的增加,可靠性也在增加,单个节点的故障将不再像小型部署一样影响重大。

复原策略


领导者故障:Raft 在可用的副本读取中选举一个新的领导者,新领导者将告知负载管理角色选举结果。副本读取故障:新节点应需加入集群。负载管理故障:上一个被加入集群的副本读取角色会接手负载管理角色的责任,且在新节点被配置之前本身不再服务于请求读取,同理,新节点会以负载管理角色加入集群。

读取密集型与写入密集型的场景


在马赫 IV 之前的阶段中,考虑到可靠系统的最低可复制配置,这套自动扩展的方案并未将负载的特性纳入考虑范围。在后续的阶段中,系统自动扩展的方式将发生变化,将通常情况下的负载区分为读取密集型(80%及以上的读取操作)、写入密集型(80%及以上的写入操作),或均衡型(其他情况)。虽然可能不包含一些特殊情况,但这个方案是客户化的模式,可以按需适应特殊场景。我们的目标只是为解决多数用例而非全部。

扩展读取密集型场景


随着新节点按照马赫 I 至 VII 阶段的模式不断被加入集群,我们需要一个最简单的策略,能够代表七个节点(一个读取管理、一个写入领导者、五个副本读取),并让集群开始以分片标签的形式创建已有数据的分片,并将传入的请求负载划分到新创建的分片之中。


在进去两个分片的运行之前,我们需要新增一个节点,达到八节点才能支持图七中所介绍的拓扑结构。

使用用例: 需求不断增长的大规模场景。


图七所展示的两个分片的设置情况,两个分片各自包含一个领导者和两个副本读取角色,并在负载管理节点中还有额外的一个副本。


图七:两个分片的拓扑结构示例


当每个分片中都配置了足够的节点,我们可以进一步扩展至三到四个分片。举例来说,如果分片二扩展至七个节点,那么接下来新增的节点将被分至第二个分片之中。


每个负载管理角色只持有一个分片的副本,负载管理角色可在需要时在该分片上处理写入或读取操作,但若想充分满足客户需求,负载管理需要从所有分片中进行读和写。

扩展写入密集型场景


对于写入操作为重点或写入操作存在合理退化的情况中,存在两种扩展集群写能力的方式:

  1. 提供一个更大的领导者第一种方式是提供比当前领导者更大的节点,“更大”可以指内存、CPU、读写存储速度等等。这一节点最初将按管理配置为负载管理,并在与当前领导者完成同步(如前文各个马赫阶段所述)之前都会作为负载管理角色存在。一旦完成同步,负载管理将开始新的选举,成为 Raft 一致性的新领导者并与旧领导者互换。集群中的每个节点都有从 1 到 5 的等级规划,其中 1 代表最低的等级规划即最少的资源,5 则是最高的即较大的资源数量。

  2. 分片一旦纵向扩展到达极限(节点等级 5),则开始分片。新的领导者被配置为副本只读,从同步上一任领导者的分片开始,逐渐成为仅负责当前分片的领导者本身,新增的节点等级(1 至 5)必须可配置。


这与前文中所描述的读取密集型分片触发方式是不同的。读取密集型分片的触发是基于水平规模(节点数),而写入密集型场景的分片触发则是基于垂直规模的,换句话说,领导者的规模到达了最大量(5)。


也可以在配置新分片时指定等级,比如让两个领导者都是等级 3,但两个等级 3 会组成一个“等级 6”的写入能力集群,不过这也应该是可配置的。在这种情况下,以前的等级 5 写入节点将被在两个分片上的两个等级 3 的节点所取代。

分片策略的标签优先级


问题点:将信息清晰有效地划分至分片并不容易,尤其是在数据量庞大或数据复杂时。


简单的解决方案:每当一条记录或对象被存储在集群中时,就给其分配一个 1 至 1000 的随机桶 ID,并确保在大规模时,每个桶都有类似数量的对象分配在其中,以均衡分片。


在需要分片时,桶 ID 可用于判断对象所属的分片。举例来说,假设桶的总数只有 1000,那么在两个分片中,第一个分片中可以仅包含第 1 至 500 个桶,第二个分片中则仅被分配第 501 至 1000 的桶。


每当需要新的分片时,都重复这个桶的划分过程。因此在我们的例子中,分片的最大数量为 1000,远超大多数情况下的正常值。

结论及未来展望


作者不奢望能通过这篇文章解决有状态系统自动扩展的所有问题,只是提供了一个基于作者职业生涯中所使用过的技术模板和模式的方案,并将其标准化,作为有状态自动扩展实现的基础。这些涉及可能不会完美适配必须对所有甚至全部分片执行大部分读取操作的场景,在这些情况下,建议采用其他更好的分片桶定义,从而尝试在一个或尽量少的分片中分配所有需要的数据。


原文链接:

https://www.infoq.com/articles/design-proposal-autoscaling-stateful-systems/

2023-03-28 09:086090

评论

发布
暂无评论
发现更多内容

Android mvvm 之 LiveData 的原理,2021年Android高级面试题

android 程序员 移动开发

Android Glide 3,android编程软件

android 程序员 移动开发

andriod搭建自己的轮询框架,flutter开发环境

android 程序员 移动开发

Android - singleTask启动模式详解,kotlin常用高阶函数

android 程序员 移动开发

Android 11 Settings源码入门,flutter安装

android 程序员 移动开发

Android Camera 内存问题剖析,Android屏幕适配很难嘛其实也就那么回事

android 程序员 移动开发

Android DataBinding 从入门到进阶,android路由实现

android 程序员 移动开发

Andorid性能优化之traceview的使用(不懂揍我),androidstudio计算器

android 程序员 移动开发

Android Systrace 使用方法,互联网寒冬

android 程序员 移动开发

Android 12 行为变更:适配以Android 12为目标的应用(1)

android 程序员 移动开发

android activity Intent 传值 传对象,移动智能终端的发展趋势

android 程序员 移动开发

Android 12 行为变更:适配以Android 12为目标的应用,移动应用开发就业方向

android 程序员 移动开发

android Alarm闹钟发送广播播放音乐,【大牛疯狂教学

android 程序员 移动开发

Android Handler 从使用到进阶,html5手机端开发

android 程序员 移动开发

Activity生命周期详解,android游戏开发实践指南

android 程序员 移动开发

Activity页面的绘制流程,移动端跨平台开发

android 程序员 移动开发

Android Matrix矩阵,跨平台移动开发ui

android 程序员 移动开发

Android MediaPlayer音频播放器详解,字节跳动高工面试

android 程序员 移动开发

Android Gradle进阶配置指南,太爽了

android 程序员 移动开发

Android GreenDao 使用全面讲解,android开发教程百度网盘

android 程序员 移动开发

Android Jetpack 之 Lifecycle - 2,2021移动开发者未来的出路在哪里

android 程序员 移动开发

Android JVM面试专题:阿里100%会问到的c,Android开发工程师面试题

android 程序员 移动开发

AdapterViewFlipper 图片_文字 轮播动画控件,【面试必会】

android 程序员 移动开发

Android 10 适配攻略,最新阿里Android面试题目

android 程序员 移动开发

Android App安装包大小优化,Android开发面试技能介绍

android 程序员 移动开发

Android Ashmem匿名共享内存,科学技术协会面试

android 程序员 移动开发

Android GreenDao 使用全面讲解(1),2021Android最新大厂面试真题总结

android 程序员 移动开发

Android 9 Pie 现已面向全球正式发布!,flutter插件播放音乐

android 程序员 移动开发

Android D8 编译器 和 R8 工具,android零基础开发

android 程序员 移动开发

Android Glide 3(1),撸了郭霖大神写的Framework源码笔记

android 程序员 移动开发

Android Gradle 干货,android屏幕适配框架

android 移动开发

有状态自动扩展系统的设计模式提议_架构_InfoQ精选文章