DevilKing's blog

冷灯看剑,剑上几分功名?炉香无需计苍生,纵一穿烟逝,万丈云埋,孤阳还照古陵

0%

原文链接

最重要的问题:

How much confidence we can have in the complex systems that we put into production?

Even when all of the individual services in a distributed system are functioning properly, the interactions between those services can cause unpredictable outcomes. Unpredictable outcomes, compounded by rare but disruptive real-world events that affect production environments, make these distributed systems inherently chaotic

不要有放松的心理

  • Chaos Engineering 的定义:是一种对分布式系统做线上的测试的实践,手段是在一个隔离的生产环境中人为制造一些极端环境。

  • 意义:即便每个组件都是好的,他们通讯出问题了,系统还是一样照挂;或者服务降级是配置有问题,超时不合适导致过多重试,服务要承受太多流量,单点故障导致雪崩。分布式系统会出现的情况实在太多了。预期等着出现,不如主动出击。主动制造问题,如果制造出的问题怎么搞系统都不会崩,那系统看起来还蛮可靠的。

  • Chaos 四个步骤:

    • 定义系统正常时的一些指标,收集监控数据。
    • 假设在 control group 和 experiment group 两个都处于正常状态。
    • 制造事故,模拟出 server crash,硬盘故障,网络连接故障等等。
    • 尝试找出 control group 和 experiment group 的状态的不同。如果有不同,那就找到了可以改进的地方。
  • 一些实践:

    • 与其搜集系统的内部属性指标,不如搜集系统的输出。吞吐,错误率,latency 百分位这些都是蛮好的指标。这些通用指标能证明系统正在工作。
    • Chaos variables 是一些跟现实中会可能是系统出现故障的那些情况,不一定会是硬件出问题,甚至网络流量出现 spike 都可能可以算。
    • 生产环境的数据很宝贵,可以做采样,然后对实验环境进行测试。
    • 对这些实验写代码,做自动化。
    • 最小化对客户的影响,限制在阈值内

原文链接

[TOC]

基础目标:

可靠、可扩展、可维护的数据系统

数据系统架构

负载:

负载可以用一些称为负载参数(load parameters)的数字来描述。参数的最佳选择取决于系统架构,它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。除此之外,也许平均情况对你很重要,也许你的瓶颈是少数极端场景。

举例,发文的时间线,有两种实现方式

  • 查数据库的方式

    1
    2
    3
    4
    5
    SELECT tweets.*, users.* 
    FROM tweets
    JOIN users ON tweets.sender_id = users.id
    JOIN follows ON follows.followee_id = users.id
    WHERE follows.follower_id = current_user

推文查询

  • 通过查询缓存的方式,为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱(图1-3)。 当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。 因此读取主页时间线的请求开销很小,因为结果已经提前计算好了

    缓存推文

    缺点在于,发推现在需要大量的额外工作。平均来说,一条推文会发往约75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入。但这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的3000万次写入!及时完成这种操作是一个巨大的挑战——推特尝试在5秒内向粉丝发送推文

最后的折中:两种方法的混合。大多数用户发推时仍然是扇出写入粉丝的主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)被排除在外。当用户读取主页时间线时,来自所关注名流的推文都会单独拉取,并与用户的主页时间线缓存合并,如方法1所示。这种混合方法能始终如一地提供良好性能

性能考量:

关于在线系统的响应时间

**一个服务100次请求响应时间的均值与百分位数**

响应时间的高百分位点(也称为尾部延迟(tail percentil))非常重要,因为它们直接影响用户的服务体验。

应对负载的方法:水平扩展,垂直扩展,弹性等等

ActiveRecord框架

网络模型 vs 关系模型

存储与检索

最简单的数据库

1
2
3
4
5
6
7
8
#!/bin/bash
db_set () {
echo "$1,$2" >> database
}

db_get () {
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

注意后面的tail -n 1取最新的一个

我们只是追加写一个文件 - 所以我们如何避免最终用完磁盘空间?一个好的解决方案是通过在达到一定大小时关闭一个段文件,然后将其写入一个新的段文件来将日志分割成特定大小的段。然后我们可以对这些段进行压缩

压缩意味着在日志中丢弃重复的键,只保留每个键的最新更新

压缩以及合并

索引类型

哈希索引

SSTable (Sorted String Table)

  • 好处:合并简单而高效,两个有序队列的合并排序;可以范围查询,因为有序;

在磁盘上维护有序结构是可能的(参阅“B-Tree”),但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构,例如红黑树或AVL树【2】。使用这些数据结构,您可以按任何顺序插入键,并按排序顺序读取它们。

现在我们可以使我们的存储引擎工作如下:

  • 写入时,将其添加到内存中的平衡树数据结构(例如,红黑树)。这个内存树有时被称为memtable。
  • 当memtable大于某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新部分。当SSTable被写入磁盘时,写入可以继续到一个新的memtable实例。
  • 为了提供读取请求,首先尝试在memtable中找到关键字,然后在最近的磁盘段中,然后在下一个较旧的段中找到该关键字。
  • 有时会在后台运行合并和压缩过程以组合段文件并丢弃覆盖或删除的值。

用SSTable制作LSM树

B树

为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:预写式日志(WAL,也称为重做日志)。这是一个只能追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来恢复B树回到一致的状态

比较B树和LSM树

根据经验,LSM树通常写速度更快,而B树被认为读取速度更快【23】

B树的写入逻辑:必须至少两次写入每一段数据:一次写入预先写入日志,一次写入树页面本身(也许再次分页)。即使在该页面中只有几个字节发生了变化,也需要一次编写整个页面的开销

LSM的缺点:

日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生请求需要等待而磁盘完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是在更高百分比的情况下,对日志结构化存储引擎的查询响应时间有时会相当长,而B树的行为则相对更具可预测性

聚集索引(clustered index)(在索引中存储所有行数据)和非聚集索引(nonclustered index)(仅在索引中存储对数据的引用)之间的折衷被称为包含列的索引(index with included columns)覆盖索引(covering index),其存储表的一部分在索引内【33】。这允许通过单独使用索引来回答一些查询(这种情况叫做:索引覆盖了(cover)查询

反直觉的是,内存数据库的性能优势并不是因为它们不需要从磁盘读取的事实。即使是基于磁盘的存储引擎也可能永远不需要从磁盘读取,因为操作系统缓存最近在内存中使用了磁盘块。相反,它们更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销

属性 事务处理 OLTP 分析系统 OLAP
主要读取模式 查询少量记录,按键读取 在大批量记录上聚合
主要写入模式 随机访问,写入要求低延时 批量导入(ETL),事件流
主要用户 终端用户,通过Web应用 内部数据分析师,决策支持
处理的数据 数据的最新状态(当前时间点) 随时间推移的历史事件
数据集尺寸 GB ~ TB TB ~ PB

编码与演化

系统想要继续顺利运行,就需要保持兼容性,向后兼容与向前兼容。

原文链接

业务场景:

  • 当订单一直处于未支付状态时,如何及时的关闭订单,并退还库存?
  • 如何定期检查处于退款状态的订单是否已经退款成功?
  • 新创建店铺,N天内没有上传商品,系统如何知道该信息,并发送激活短信?等等

设计目标:

  • 消息传输可靠性:消息进入到延迟队列后,保证至少被消费一次。
  • Client支持丰富:由于业务上的需求,至少支持PHP和Python。
  • 高可用性:至少得支持多实例部署。挂掉一个实例后,还有后备实例继续提供服务。
  • 实时性:允许存在一定的时间误差。
  • 支持消息删除:业务使用方,可以随时删除指定消息。

设计结构

msg结构:

  • Topic:Job类型。可以理解成具体的业务名称。
  • Id:Job的唯一标识。用来检索和删除指定的Job信息。
  • Delay:Job需要延迟的时间。单位:秒。(服务端会将其转换为绝对时间)
  • TTR(time-to-run):Job执行超时时间。单位:秒。
  • Body:Job的内容,供消费者做具体的业务处理,以json格式存储。

一个msg的生命周期:

  • 用户对某个商品下单,系统创建订单成功,同时往延迟队列里put一个job。job结构为:{‘topic’:’orderclose’, ‘id’:’ordercloseorderNoXXX’, ‘delay’:1800 ,’TTR’:60 , ‘body’:’XXXXXXX’}
  • 延迟队列收到该job后,先往job pool中存入job信息,然后根据delay计算出绝对执行时间,并以轮询(round-robbin)的方式将job id放入某个bucket。
  • timer每时每刻都在轮询各个bucket,当1800秒(30分钟)过后,检查到上面的job的执行时间到了,取得job id从job pool中获取元信息。如果这时该job处于deleted状态,则pass,继续做轮询;如果job处于非deleted状态,首先再次确认元信息中delay是否大于等于当前时间,如果满足则根据topic将job id放入对应的ready queue,然后从bucket中移除;如果不满足则重新计算delay时间,再次放入bucket,并将之前的job id从bucket中移除。
  • 消费端轮询对应的topic的ready queue(这里仍然要判断该job的合理性),获取job后做自己的业务逻辑。与此同时,服务端将已经被消费端获取的job按照其设定的TTR,重新计算执行时间,并将其放入bucket。
  • 消费端处理完业务后向服务端响应finish,服务端根据job id删除对应的元信息。

  • timer是通过独立线程的无限循环来实现,在没有ready job的时候会对CPU造成一定的浪费。
  • 消费端在reserve job的时候,采用的是http短轮询的方式,且每次只能取的一个job。如果ready job较多的时候会加大网络I/O的消耗。
  • 数据存储使用的redis,消息在持久化上受限于redis的特性。

  • 基于wait/notify方式的Timer实现

  • 提供TCP长连的API,实现push或者long-polling的消息reserve方法

  • 拥有自己的存储方案(内嵌数据库、自定义数据结构写文件),确保消息的持久化

  • 考虑提供周期性任务的直接支持

原文链接

可以发现Gson序列化占用了大部分的执行时间,从图2可以更直观地看到Gson.fromJson占用了61%的执行时间。分析Gson的源码可以发现,它在序列化时大量使用了反射,每一个field,每一个get、set都需要用反射,由此带来了性能问题。

减少反射

采用JSONObject的方式来做序列化,

简单且性能好的

采用AnnotationProcessor(注解处理器)的方式,找到有JsonType注解的bean来处理,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(fullClassName);
ClassModel classModel = new ClassModel().setModifier("public final").setClassName(simpleClassName);
......
JavaFile javaFile = new JavaFile();
javaFile.setPackageModel(new PackageModel().setPackageName(packageName))
.setImportModel(new ImportModel()
.addImport(elementClassName)
.addImport("com.meituan.android.MSON.IJsonObject")
.addImport("com.meituan.android.MSON.IJsonArray")
.addImport("com.meituan.android.MSON.exceptions.JsonParseException")
.addImports(extension.getImportList())
).setClassModel(classModel);

List<? extends Element> enclosedElements = element.getEnclosedElements();
for (Element e : enclosedElements) {
if (e.getKind() == ElementKind.FIELD) {
processFieldElement(e, extension, toJsonMethodBlock, fromJsonMethodBlock);
}
}
try (Writer writer = sourceFile.openWriter()) {
writer.write(javaFile.toSourceString());
writer.flush();
writer.close();
}

继续优化

当JSON数据量比较大时用JSONObject处理会比较慢,究其原因是JSONObject会一次性将字符串读进来解析成一个map,这样会有比较大的内存浪费和频繁内存创建。经过调研Gson内部的实现细节,发现Gson底层有流式的解析器而且可以按需解析,可以做到匹配上的字段才去解析。根据这个发现我们将我们IJSONObject和IJsonArray换成了Gson底层的流解析来进一步优化我们的速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Friend object = new Friend();
reader.beginObject();
while (reader.hasNext()) {
String field = reader.nextName();
if ("id".equals(field)) {
object.id = reader.nextInt();
} else if ("name".equals(field)) {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
object.name = null;
} else {
object.name = reader.nextString();
}
} else {
reader.skipValue();
}
}
reader.endObject();

兼容性

兼容性主要体现在能支持的数据类型上,目前MSON支持了基础数据类型,包装类型、枚举、数组、List、Set、Map、SparseArray以及各种嵌套类型(比如:Map<String, Map<String, List<String[]>>>

结论上

兼容性上,mson最好

性能上,mson也较少耗时,Gson和fastjson的耗时相当

链式法则

fg为两个关于x可导函数,则有下面的公式:
$$
(f\circ g)’(x)=f’(g(x))g’(x)
$$
考虑函数z = f(x, y),其中x = g(t),y = h(t),g(t)和h(t)是可微函数
$$
\frac{dz}{dx} = \frac{\partial z}{\partial x} \frac{\partial x}{\partial t} + \frac{\partial z}{\partial y} \frac{\partial y}{\partial t}
$$
这个公式是作为梯度下降算法的基本参照