分类目录归档:代码相关

code

api变更怎么搞

api的变更在库的开发维护过程中非常常见,库使用者很少时,直接强制更新没毛病,用的人多了,影响就会被放大,此时兼容就成了不得不考虑的事情。目前我大概见过几种比较常见的变更做法。

  1. 完全兼容。
    新api随便怎么搞,但要为老接口单独做一份兼容。例如libevent会单独将一些需要兼容的api统一放到一个特定的头文件中暴露出来。
  2. 编译时提醒。
    比如c++里面,可以直接把老接口的函数头加上__attribute__((deprecated(msg)))的修饰,用于提示新的替代api在哪里或者给出建议,当然也可以根据其他的预编译宏#error, #warning, #pragma message(msg)等来给出相应的提示。
  3. 文档化。
    每个版本或者库文档,说明的地方罗列好changes和api。总体说来,“完全兼容“通常对维护者是个包袱对用户是个便利,但是若原接口有隐含bug或者性能问题的时候,就不得不强制用户使用新api了。“编译提醒”是个折中,通常无需用户感知库的具体变化根据提示就能完成切换工作。“文档化”则直接把工作全部推给使用者了,通常老用户最为反感,工作量较大,一般在出现在大版本或存在重大特性无法进行兼容的场景下。

定时器接口优化之function和bind

框架早年的timer是通过一系列函数指针来封装回调函数,期间使用0x的function和bind优化了一波,极大的简化了代码,但是我们的timer回调函数的形式仍然保持着void timer_callback(int fd, short event, void *arg);的形式(仿libevent与普通fd事件回调函数保持一致)。但在大量的开发实践中,绝大部分用不到其中任何一个参数,极少部分会使用到arg,而我们仍然不得不完整的写完整个函数头部。经过分析发现我们可以利用function和bind的一些“特质“来再度优化这个繁琐的回调范式。

  1. std::function并不校验接受到的function跟自身type是否一致,仅在被执行时检查可调用实体的形式是否匹配,这一特性让我们可以用来兼容任意回调形式
  2. std::bind并不是只能bind函数,还可以嵌套bind
  3. 让timer支持任意类型。
    1) timer内部持有的function切换成std::function<void(void)>,通过内部嵌套bind来兼容原回调形式void callback_old(int, short, void*);
    2)timer接口支持任意类型(AddTimer(std::function<void(void)>)),只需bind时具名所有参数即可(例如void callback_xxxx(int type, void *arg); AddTimer(std::bind(callback_xxxx, type, ptr)))。

in-source builds or out-source builds

最近一段时间在跟进解决一系列的跟编译相关的事情,发现大家特别喜欢在源码目录直接编译代码,功能角度上这没有什么问题,但从源码管理角度来看这种方式还是存在很多不好的地方,最直观的感受就是一堆凌乱的编译中间文件。那么为什么我们应该选择out-source builds而不是in-source builds呢?(这篇文章列得很详细http://voices.canonical.com/jussi.pakkanen/2013/04/16/why-you-should-consider-using-separate-build-directories/)

1. 易于清理
这个是最容易理解也最显而易见的,一堆中间临时文件,干扰你阅读源码,干扰你打包源码tree,干扰你清理编译中间文件。

2. 可以针对不同的编译选项设立独立的编译目录
这样的好处显而易见,可能做测试,可能给不同类型的用户使用,需要多套不同的编译选项,这样in-source builds就不能满足要求了。

3. 可以对源码目录和编译目录做特定控制或优化
比如源码不允许修改(只读),或者需要加速编译,可能会把编译目录放到更快速的存储设备上。

向目标文件注入编译环境信息

【背景】
前段时间一个离职同事几年前的老模块需要交接,由于长时间无人维护,一时找不到代码路径,只得从整个后台中心的代码库里面“人肉“。记得曾经还出现过类似的情况,找不到老的编译环境(现有的系统,机器都升级了),被迫重构了老代码。工作时间越长,这样的问题也就会越来越频繁,老是这么死板的处理也不是个办法,费时费力,苦不堪言。如果可以将server部署前的编译环境信息集成到目标文件中,是否就可以根据线上运行的目标文件定位到相关的信息呢?目前部门的开发方式都是依赖于框架,所以只得在框架上下功夫。定好2个核心原则,撸起袖子就是干:对开发者透明,能便捷的获取信息。

【实现】
整个框架的编译体系是通过cmake维系的,通过其丰富的能力可以实现一些特殊的功效。框架使用gflags来维护命令行参数,本已经集成一些基本的参数,比如threads, conf,pid_file等,所以直接通过这种方式来获取编译环境信息对开发者更加熟悉和便捷。
1. cmake获取编译环境信息。

2. cmake将信息string以宏的方式透传给框架头文件。

3. 框架头文件使用宏来区分框架编译和业务编译。

4. 运行效果。

 

 

控制cmake查询路径

【背景】
部门依赖特定框架开发代码,框架中已经集成了protobuf,但是用户却有可能在开发机上胡乱安装protobuf。而业务链接到的版本是框架的protobuf,但cmake中find_package(Protobuf)默认的是用系统protobuf中的protoc编译描述文件,当系统中pb和框架pb版本不同时,就会造成编译错误。

【构思】
研究了下Cmake中的FindProtobuf.cmake文件,_protobuf_find_libraries|_protobuf_find_libraries|_protobuf_find_libraries中查找lib,include,bin均是通过find_path,find_program,find_library来完成的,只要控制查找路径即可。

于是,框架的xxx-config.cmake中添加一下两行解决问题。业务依赖框架时find_pakcage(xxx)就会自动设置cmake的路径变量,最终达到控制查询结果的目的。

 

框架自动化部署

【背景】
部门框架是需要部署到开发机上的,框架的更新一直是手动到每台开发机上去更新部署,开发人员和开发机多了之后,框架维护人员已经很难管理住所有开发机的框架环境了。

【构思】
一切只围绕一个目的:在框架有更新时能够更新开发机的框架环境。总体思路是利用CMAKE的ExternalProject_Add来维护项目的编译,crontab来保证检查执行:设定最新的tag_url(如果与历史svn_url不一样就svn switch),svn up + cmake + make。

 

【后续】
现在能够自动更新了,后面最好能够呈现开发机框架视图,所以还需要上报本机的框架环境信息,在服务端呈现出来。

glog FATAL日志写多次的问题

【背景】
LOG(FATAL)能够使代码更加简洁,在项目中用得比较多。但实际真正触发的时候,看日志总感觉没有写进去。去日志目录观察,实际上是写了两份日志,而当前的日志软连接链到了空日志文件上。这在日志目录下日志文件比较多时,查看上一次挂掉的日志非常不方便。
glog原始情况

【研究】
实际上阅读glog代码发现SendToLog这个函数在打印完所有日志之后,针对FATAL级别的日志,又重新写了一次,而且时间戳是0即(19700101-080000)。

 【解决】
注释掉该段代码搞定。
glog-after-fix

记一次负载均衡不生效的问题

【背景】
公司目前使用一种基于调用方反馈来衡量后端服务的负载均衡组件,姑且称之为LB_T。正常情况下后端服务异常都会让调用方将此问题机器M_T踢掉,因为每一次成功的调用,调用方都会上报一个成功状态和时耗用以对后端可用性和负载做统计。

【问题】
但这一次只看到后端一直返回错误,而调用方却仍然再往这台机器发包。在基础组件上没有找到太多蛛丝马迹,而后进一步排查发现后端过载了仍然会向前端回包。这也就解释得通了,LB_T这种LB方式通常后端有回包就会认为是成功。虽然可以特化处理错误码,但为了整体的通用性,修改框架让后端过载时只上报自身服务质量和记录日志,不向前端回包,问题解决。

【思考】
问题虽然解决,但是引发了我们的思考:代码中使用了LB组件就一定能做到负载均衡,异常时能切换能容灾?答案是否定的:
1. 姿势非常关键。每一次请求都必定要对LB进行一次Update。
2. 上下游配合。一定要清楚的知道后端何为异常,对后端足够的了解,相互配合才能达到效果。

 

 

没有C++11我们如何实现c++的函数变参

传统的变参方法都是基于上古的VA_ARGS来实现的,c++11引入了变参模板,把编写变参方法的便利度提高了的一个前所未有的高度。re2这个库很好的诠释了C++里面即便没有变参模板,也能做便利的变参实现。

【背景】

FullMatch需要提取匹配到的所有子串,而正则表达式本身的灵活性决定了这里的参数(匹配到的子串)必然是变化的。

【RE2实现】

【使用c++11变参模板的实现】

RE2的变参实现通过整齐的重载operator()来实现,最大支持32个参数,因为只实现了32个重载。
而反观使用C++11的变参模板,则简化了很多代码,且没有参数32的限制。后者的便利性自然是极高的,但是在编译器的通用性上,前者更加通用。在没有C++11的情况下,google::re2又给出了实现变参的很好的一个例子。

轮server监控的层次

监控是服务的晴雨表,对开发者尤为重要,基本是server上线的标配,主要分两个层次。

1. 标准化监控
这里主要有两类 : 标准化的网络设备(主机监控),例如路由器交换机,通常都可以通过snmp协议采集到(即便是私有的od指标也能从生产商处获得);标准化的服务(中间件监控),例如httpd,nginx,mysql,web页面等等,通过对标准服务的特性采集,例如运行端口,进程名称,日志情况等来配置监控参数。
标准化维度的监控,绝大部分开源软件(zabbix,cacti等)都都能满足。

2. 业务服务监控
具体的业务监控,开源的东东就捉襟见肘了,仅仅少部分场景能通过脚本勉强满足,这就是为什么大部分成熟的互联网公司都有自己的一套或几套监控系统。监控 = 上报 + 绘图 + 告警,监控系统 = 上报api + 视图管理 + 告警管理。这里面又大致分成两大类 : 数据量维度的监控和状态维度的监控。

1). 数据量维度
上报点包括总请求量,总失败量,总成功量,各分支请求量,各分支失败量,各分支成功量,特殊上报量,时延区间量(例如50ms以下的处理请求量等)。
数据量是业务监控的最低层次,各项指标不正常,server必然是有问题,但指标正常并不代表server没有问题。

a) server嵌入上报api,monitor_api(int attr_id, int attr_value)
b) monitor_agent周期汇总本机的所有的attr_id的上报值发送给collect_svr
c) collect_svr对数据做处理,按(attr_id, report_ip) -> attr_value存储数据,按attr_id -> report_ip_list和report_ip -> attr_id_list存储索引关系
d) 页面上根据数据和索引关系聚合出各种视图 (attr_id, report_ip, day_timeline),(attr_id, all_report_ip, day_timeline),(report_ip, attr_id_list, day_timeline)
e) 告警模块中针对各个视图关系设置最大值/最小值等告警策略

2). 状态维度
服务的质量通常都是状态量,最常见的指标就是 时延和成功率。这两个指标基本可以反映出某条服务或接口的服务质量。

a) server调用后端结束时嵌入上报api,stat_api(int service_id, int stat_code, uint32_t cost_time)
b) stat_agent周期汇总或者直接转发本机的所有的service_id的上报数据发送给collect_svr
c) collect_svr对数据做处理,按(service_id, called_ip, report_ip) -> stat_value存储数据,按(service_id, called_ip) -> report_ip_list, (service_id, report_ip) -> called_ip_list存储索引
d) 页面上根据数据和索引关系聚合出各种视图 (service_id, report_ip, called_ip_list, day_timeline),(attr_id, called_ip, report_ip_list, day_timeline),(service_id, report_ip, called_ip, day_timeline)
e) 告警模块中对单机或者整个服务的 时延或成功率设置阀值

3. 调用链监控
仅仅是服务自身的监控是不够,还得能够拓扑出一条请求的来龙去脉。这里又分两种:这里业界通用做法都是通过一个traceId贯穿整个调用路径,然后由后端的日志平台离线分析去统计;另外前者必须保证整个公司所有路径都要规范上报,有时候比较困难,局部实现的话,在某一个程序上以调用者的视角去统计所有服务接口的各种信息去上报统计拓扑更容易实施。