1. 故障恢复方法 告警
测试环境中出现了一个异常的告警现象:一条告警通过 Thanos Ruler 的 HTTP 接口观察到持续处于 active 状态,但是从 AlertManager 这边看这条告警为已解决状态。按照 DMP 平台的设计,告警已解决指的是告警上设置的结束时间已经过了当前时间。一条发送至 AlertManager 的告警为已解决状态有三种可能:1. 手动解决了告警2. 告警只产生了一次,第二次计算告警规则时会发送一个已解决的告警3. AlertManager 接收到的告警会带着一个自动解决时间,如果还没到达自动解决时间,则将该时间重置为 24h 后首先,因为了解到测试环境没有手动解决过异常告警,排除第一条;其次,由于该告警持续处于 active 状态,所以不会是因为告警只产生了一次而接收到已解决状态的告警,排除第二条;最后,告警的告警的产生时间与自动解决时间相差不是 24h,排除第三条。那问题出在什么地方呢?
分析
下面我们开始分析这个问题。综合第一节的描述,初步的猜想是告警在到达 AlertManager 前的某些阶段的处理过程太长,导致告警到达 AlertManager 后就已经过了自动解决时间。我们从分析平台里一条告警的流转过程入手,找出告警在哪个处理阶段耗时过长。首先,一条告警的产生需要两方面的配合:
metric 数据
告警规则
将 metric 数据输入到告警规则进行计算,如果符合条件则产生告警。DMP 平台集成了 Thanos 的相关组件,数据的提供和计算则会分开,数据还是由 Prometheus Server 提供,而告警规则的计算则交由 Thanos Rule(下文简称 Ruler)处理。下图是 Ruler 组件在集群中所处的位置:

首先,图中每个告警规则 Rule 都有一个 active queue(下面简称本地队列),用来保存一个告警规则下的活跃告警。
其次,从本地队列中取出告警,发送至 AlertManager 前,会被放入 Thanos Rule Queue(下面简称缓冲队列),该缓冲队列有两个属性:
capacity(默认值为 10000):控制缓冲队列的大小,
maxBatchSize(默认值为 100):控制单次发送到 AlertManager 的最大告警数
了解了上述过程,再通过翻阅 Ruler 源码发现,一条告警在放入缓冲队列前,会为其设置一个默认的自动解决时间(当前时间 + 3m),这里是影响告警自动解决的开始时间,在这以后,有两个阶段可能影响告警的处理:1.缓冲队列阶段2.出缓冲队列到 AlertManager 阶段(网络延迟影响)由于测试环境是局域网环境,并且也没在环境上发现网络相关的问题,我们初步排除第二个阶段的影响,下面我们将注意力放在缓冲队列上。通过相关源码发现,告警在缓冲队列中的处理过程大致如下:如果本地队列中存在一条告警,其上次发送之间距离现在超过了 1m(默认值,可修改),则将该告警放入缓冲队列,并从缓冲队列中推送最多 maxBatchSize 个告警发送至 AlertManager。反之,如果所有本地队列中的告警,在最近 1m 内都有发送过,那么就不会推送缓冲队列中的告警。也就是说,如果在一段时间内,产生了大量重复的告警,缓冲队列的推送频率会下降。队列的生产方太多,消费方太少,该队列中的告警就会产生堆积的现象。因此我们不难猜测,问题原因很可能是是缓冲队列推送频率变低的情况下,单次推送的告警数量太少,导致缓冲队列堆积。下面我们通过两个方面验证上述猜想:首先通过日志可以得到队列在大约 20000s 内推送了大约 2000 次,即平均 10s 推送一次。结合缓冲队列的具体属性,一条存在于队列中的告警大约需要 (capacity/maxBatchSize)*10s = 16m,AlertManager 在接收到告警后早已超过了默认的自动解决时间(3m)。其次,Ruler 提供了 3 个 metric 的值来监控缓冲队列的运行情况:
thanos_alert_queue_alerts_dropped_total
thanos_alert_queue_alerts_pushed_total
thanos_alert_queue_alerts_popped_total
通过观察 thanos_alert_queue_alerts_dropped_total 的值,看到存在告警丢失的总数,也能佐证了缓冲队列在某些时刻存在已满的情况。
解决通过以上的分析,我们基本确定了问题的根源:Ruler 组件内置的缓冲队列堆积造成了告警发送的延迟。针对这个问题,我们选择调整队列的 maxBatchSize 值。下面介绍一下这个值如何设置的思路。由于每计算一次告警规则就会尝试推送一次缓冲队列,我们通过估计一个告警数量的最大值,得到 maxBatchSize 可以设置的最小值。假设你的业务系统需要监控的实体数量分别为 x1、x2、x3、...、xn,实体上的告警规则数量分别有 y1、y2、y3、...、yn,那么一次能产生的告警数量最多是(x1 * y2 + x2 * y2 + x3 * y3 + ... + xn * yn),最多推送(y1 + y2 + y3 + ... + yn)次,所以要使缓冲队列不堆积,maxBatchSize 应该满足:maxBatchSize >= (x1 * y2 + x2 * y2 + x3 * y3 + ... + xn * yn) / (y1 + y2 + y3 + ... + yn),假设 x = max(x1,x2, ...,xn), 将不等式右边适当放大后为 x,即 maxBatchSize 的最小值为 x。也就是说,可以将 maxBatchSize 设置为系统中数量最大的那一类监控实体,对于 DMP 平台,一般来说是 MySQL 实例。
注意事项
上面的计算过程只是提供一个参考思路,如果最终计算出该值过大,很有可能对 AlertManager 造成压力,因而失去缓冲队列的作用,所以还是需要结合实际情况,具体分析。因为 DMP 将 Ruler 集成到了自己的组件中,所以可以比较方便地对这个值进行修改。如果是依照官方文档的介绍使用的 Ruler 组件,那么需要对源码文件进行定制化修改。
2. react native 怎么获取服务器ksoap协议的数据
React Native用iOS自带的JavaScriptCore作为JS的解析引擎,但并没有用到JavaScriptCore提供的一些可以让JS与OC互调的特性,而是自己实现了一套机制,这套机制可以通用于所有JS引擎上,在没有JavaScriptCore的情况下也可以用webview代替,实际上项目里就已经有了用webview作为解析引擎的实现,应该是用于兼容iOS7以下没有JavascriptCore的版本。
普通的JS-OC通信实际上很简单,OC向JS传信息有现成的接口,像webview提供的-方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息。React Native也是以此为基础,通过各种手段,实现了在OC定义一个模块方法,JS可以直接调用这个模块方法并还可以无缝衔接回调。
举个例子,OC定义了一个模块RCTSQLManager,里面有个方法-query:successCallback:,JS可以直接调用RCTSQLManager.query并通过回调获取执行结果。:
1
2
3
4
5
6
7
8
9
//OC:
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
RCT_EXPORT();
NSString *ret = @"ret"
responseSender(ret);
}
@end
1
2
3
4
//JS:
RCTSQLManager.query("SELECT * FROM table", function(result) {
//result == "ret";
});
接下来看看它是怎样实现的。
模块配置表
首先OC要告诉JS它有什么模块,模块里有什么方法,JS才知道有这些方法后才有可能去调用这些方法。这里的实现是OC生成一份模块配置表传给JS,配置表里包括了所有模块和模块里方法的信息。例:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
{
"remoteMoleConfig": {
"RCTSQLManager": {
"methods": {
"query": {
"type": "remote",
"methodID": 0
}
},
"moleID": 4
},
...
},
}
OC端和JS端分别各有一个bridge,两个bridge都保存了同样一份模块配置表,JS调用OC模块方法时,通过bridge里的配置表把模块方法转为模块ID和方法ID传给OC,OC通过bridge的模块配置表找到对应的方法执行之,以上述代码为例,流程大概是这样(先不考虑callback):
在了解这个调用流程之前,我们先来看看OC的模块配置表式怎么来的。我们在新建一个OC模块时,JS和OC都不需要为新的模块手动去某个地方添加一些配置,模块配置表是自动生成的,只要项目里有一个模块,就会把这个模块加到配置表上,那这个模块配置表是怎样自动生成的呢?分两个步骤:
1.取所有模块类
每个模块类都实现了RCTBridgeMole接口,可以通过runtime接口objc_getClassList或objc_ClassList取出项目里所有类,然后逐个判断是否实现了RCTBridgeMole接口,就可以找到所有模块类,实现在RCTBridgeMoleClassesByMoleID()方法里。
2.取模块里暴露给JS的方法
一个模块里可以有很多方法,一些是可以暴露给JS直接调用的,一些是私有的不想暴露给JS,怎样做到提取这些暴露的方法呢?我能想到的方法是对要暴露的方法名制定一些规则,比如用RCTExport_作为前缀,然后用runtime方法class_getInstanceMethod取出所有方法名字,提取以RCTExport_为前缀的方法,但这样做恶心的地方是每个方法必须加前缀。React Native用了另一种黑魔法似的方法解决这个问题:编译属性__attribute__。
在上述例子中我们看到模块方法里有句代码:RCT_EXPORT(),模块里的方法加上这个宏就可以实现暴露给JS,无需其他规则,那这个宏做了什么呢?来看看它的定义:
1
2
#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }
这个宏的作用是用编译属性__attribute__给二进制文件新建一个section,属于__DATA数据段,名字为RCTExport,并在这个段里加入当前方法名。编译器在编译时会找到__attribute__进行处理,为生成的可执行文件加入相应的内容。效果可以从linkmap看出来:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
# Sections:
# Address Size Segment Section
0x100001670 0x000C0180 __TEXT __text
...
0x10011EFA0 0x00000330 __DATA RCTExport
0x10011F2D0 0x00000010 __DATA __common
0x10011F2E0 0x000003B8 __DATA __bss
...
0x10011EFA0 0x00000010 [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__
0x10011EFB0 0x00000010 [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__
0x10011EFC0 0x00000010 [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__
0x10011EFD0 0x00000010 [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__
...
可以看到可执行文件数据段多了个RCTExport段,内容就是各个要暴露给JS的方法。这些内容是可以在运行时获取到的,在RCTBridge.m的RCTExportedMethodsByMoleID()方法里获取这些内容,提取每个方法的类名和方法名,就完成了提取模块里暴露给JS方法的工作。
整体的模块类/方法提取实现在RCTRemoteMolesConfig()方法里。
调用流程
接下来看看JS调用OC模块方法的详细流程,包括callback回调。这时需要细化一下上述的调用流程图:
看起来有点复杂,不过一步步说明,应该很容易弄清楚整个流程,图中每个流程都标了序号,从发起调用到执行回调总共有11个步骤,详细说明下这些步骤:
1.JS端调用某个OC模块暴露出来的方法。
2.把上一步的调用分解为MoleName,MethodName,arguments,再扔给MessageQueue处理。
在初始化时模块配置表上的每一个模块都生成了对应的remoteMole对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的_createBridgedMole里,整个实现区区24行代码,感受下JS的魔力吧。
3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的MoleName和MethodName转为MoleID和MethodID。
4.把上述步骤得到的MoleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。
5.OC接收到消息,通过模块配置表拿到对应的模块和方法。
实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的RCTMoleMethod对象,这里通过MoleID和MethodID取到对应的Mole实例和RCTMoleMethod实例进行调用。具体实现在_handleRequestNumber:moleID:methodID:params:。
6.RCTMoleMethod对JS传过来的每一个参数进行处理。
RCTMoleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。
例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。
这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。
7.OC模块方法调用完,执行block回调。
8.调用到第6步说明的RCTMoleMethod生成的block。
9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法。
10.MessageQueue通过CallbackID找到相应的JS callback方法。
11.调用callback方法,并把OC带过来的参数一起传过去,完成回调。
整个流程就是这样,简单概括下,差不多就是:JS函数调用转MoleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行
事件响应
上述第4步留下一个问题,JS是怎样把数据传给OC,让OC去调相应方法的?
答案是通过返回值。JS不会主动传递数据给OC,在调OC方法时,会在上述第4步把MoleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。
一开始不明白,设计成JS无法直接调用OC,需要在OC去调JS时才通过返回值触发调用,整个程序还能跑得通吗。后来想想纯native开发里的事件响应机制,就有点理解了。native开发里,什么时候会执行代码?只在有事件触发的时候,这个事件可以是启动事件,触摸事件,timer事件,系统事件,回调事件。而在React Native里,这些事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的,这跟native开发里事件响应机制是一致的。
说到OC调用JS,再补充一下,实际上模块配置表除了有上述OC的模块remoteMoles外,还保存了JS模块localMoles,OC调JS某些模块的方法时,也是通过传递MoleID和MethodID去调用的,都会走到-enqueueJSCall:args:方法把两个ID和参数传给JS的BatchedBridge.,跟JS调OC原理差不多,就不再赘述了。