Thrift 个人实战--Thrift RPC服务框架日志的优化
前言:
Thrift作为Facebook开源的RPC框架, 通过IDL中间语言, 并借助代码生成引擎生成各种主流语言的rpc框架服务端/客户端代码. 不过Thrift的实现, 简单使用离实际生产环境还是有一定距离, 本系列将对Thrift作代码解读和框架扩充, 使得它更加贴近生产环境. 本文讲述RPC服务框架中, 日志的重要性, 以及logid的引入. 日志不仅包含丰富的数据(就看是否会挖掘), 而且还是线上服务问题追踪和排查错误最好的方式.
日志级别
采用大家喜闻乐见的log4j作为该RPC服务框架首选的日志库. 其对日志的级别有如下几种:
1). TRACE 最细粒度级别日志级别
2). DEBUG 对调试应用程序有帮助的日志级别
3). INFO 粗粒度级别突出强调应用程序的运行
4). WARN 表明潜在错误的情形
5). ERROR 明确发生错误, 但不影响系统继续运行
6). FATAL 严重的错误, 会导致应用工作不正常
日志级别等级顺序如下: TRACE < DEBUG < INFO < WARN < ERROR < FATAL
而应用具体的输出取决于日志级别的设置(包含及以上才会输出), 往往项目该上线采用DEBUG级别(日志量大, 容易写满磁盘), 等系统稳定后采用INFO级别
RPC服务日志需求
上述的日志需求虽然能定位问题, 但往往存在如下问题:
1). 很多日志只是简单了记录该点(代码行)运行过, 或是运行到该点的数据快照.
2). 服务由多种模块(每个模块由有多个节点构成)组成, 之间的日志串联不起来.
而好的日志设计, 必须能满足
1). 以完整的一次RPC调用作为单位(不是某个执行点快照, 而是完整的RPC callback过程), 并输出完整的一行日志记录, 包括(时间点, 来源, 输入参数, 输出参数, 中间经历的子过程, 消耗时间).
2). 引入logid, 作为多个模块之间串联的依据.
RPC级别的日志解决方案
尝试如下navie的方式去实现
public String echo(String msg) {
StringBuilder sb = new StringBuilder();
// *) 记录输入参数
sb.append("[request: {msg: msg}]");
// *) 访问缓存服务
sb.append("[action: access redis, consume 100ms]");
// *). 访问后端数据库
sb.append("[action: dao, consume 100ms]");
// *). 记录返回结果
sb.append("[response: {msg}]");
logger.info(sb.toString());
return msg;
}
评注: 这边的echo函数代表了一个rpc服务调用接口, 且简化了各个组件的交互. 同时引入StringBuilder, 记录各个交互的过程和时间消耗, 最后统一由函数出口前使用logger进行日志的统一输入.
但是这种方式弊端非常的明显:
1. 假设该rpc服务的函数, 存在多个出口
2. 函数存在嵌套调用, 需要嵌套子函数的过程信息
如下面的代码片段, 可参考:
public boolean verifySession() {
// ***********我要记录日志(*^__^*) ***************
}
public String echo(String msg) {
StringBuilder sb = new StringBuilder();
// *) 调用子过程
verifySession();
// *) 记录输入参数
sb.append("[request: {msg: msg}]");
// *) 访问缓存服务
if ( KeyValueEngine Access Fail ) {
// *********日志记录在那里***********
throw new Exception();
}
sb.append("[action: access redis, consume 100ms]");
// *). 访问后端数据库
if ( Database Access Fail ) {
// *********日志记录在那里***********
throw new Exception();
}
sb.append("[action: dao, consume 100ms]");
// *). 记录返回结果
sb.append("[response: {msg}]");
logger.info(sb.toString());
return msg;
}
评注: 子函数verifySession的调用, 需要把StringBuilder对象往里传, 才能记录相关的信息, 而多个异常出口, 需要把日志输入往里添加(这个繁琐且容易忘记). 这种方案只能说容易想到, 但不是最佳的方案.
有一点不可否认, rpc调用始终在同一个线程中. 聪明的读者是否猜到了最佳的解决方案.
对, 就是大杀器ThreadLocal,其能解决子函数调用的问题, 那多出口问题呢? 让rpc服务框架去处理, 其作为具体rpc调用的最外层.
采用动态代理类, 去拦截rpc的handler接口调用.
public class LogProxyHandler<T> implements InvocationHandler {
private T instance;
public LogProxyHandler(T instance) {
this.instance = instance;
}
public Object createProxy() {
return Proxy.newProxyInstance(instance.getClass().getClassLoader(),
instance.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// *) 函数调用前, 拦截处理, 作ThreadLocal的初始化工作
LoggerUtility.beforeInvoke(); // -----(1)
try {
Object res = method.invoke(instance, args);
// *) 函数成功返回后, 拦截处理, 进行日志的集中输出
LoggerUtility.returnInvoke(); // -----(2)
return res;
} catch (Throwable e) {
// *) 出现异常后, 拦截处理, 进行日志集中输入 // -----(3)
LoggerUtility.throwableInvode("[result = exception: {%s}]", e.getMessage());
throw e;
}
}
}
代码评注:
(1). 拦截点beforeInvoke用于ThreadLocal的初始话工作, 日志缓存的清空
(2). 拦截点returnInvoke用于函数成功返回后, 进行日志集中输出
(3). 拦截点throwableInvoke用于出现异常后, 进行日志的集中输出
同时在rpc服务调用中, 才用LoggerUtility的noticeLog静态函数(简单缓存中间日志过程)代替之前的StringBuilder.append来记录中间子过程
LoggerUtility的代码如下所示:
public class LoggerUtility {
private static final Logger rpcLogger = LoggerFactory.getLogger("rpc");
public static final ThreadLocal<StringBuilder> threadLocals = new ThreadLocal<StringBuilder>();
public static void beforeInvoke() {
StringBuilder sb = threadLocals.get();
if ( sb == null ) {
sb = new StringBuilder();
threadLocals.set(sb);
}
sb.delete(0, sb.length());
}
public static void returnInvoke() {
StringBuilder sb = threadLocals.get();
if ( sb != null ) {
rpcLogger.info(sb.toString());
}
}
public static void throwableInvode(String fmt, Object... args) {
StringBuilder sb = threadLocals.get();
if ( sb != null ) {
rpcLogger.info(sb.toString() + " " + String.format(fmt, args));
}
}
public static void noticeLog(String fmt, Object... args) {
StringBuilder sb = threadLocals.get();
if ( sb != null ) {
sb.append(String.format(fmt, args));
}
}
}
两者的结合完美的解决了上述RPC的日志问题, 是不是很赞.
Logid的日志解决方案
Thrift框架本身是没有logid的概念的, 我们很难去改动thrift的rpc协议, 去添加它(比如大百度的做法是把logid作为rpc协议本身一部分). 这边的解决方案是基于约定. 我们采用如下约定, 所有的rpc请求参数都封装为一个具体Request对象, 所有的返回结构都封装为一个具体的Response对象, 而每个Request对象首个属性是logid.
比如如下的结构定义:
struct EchoRequest {
1: required i64 logid = 1001,
2: required string msg
}
struct EchoResponse {
1: required i32 status,
2: optional string msg
}
service EchoService {
EchoResponse echo(1: EchoRequest req);
}
评注: Request结构中logid, 就是约定的需要加到rpc的请求结构里去的.
我一直觉得: 约定优于配置, 约定优于框架.
后续
中间插入日志处理这块, 后续讲述之前计划的服务发布/订阅化, 借助zookeeper来构建一个简单的系统, 敬请期待.
Thrift 个人实战--Thrift RPC服务框架日志的优化的更多相关文章
- RPC服务框架探索之Thrift
前言架构服务化后,需要实现一套方便调用各服务的框架,现在开源如日中天,优先会寻找开源实现,如果没有合适自家公司业务的,才会考虑从零开发,尤其是一切以KPI为准绳的公司,谁会跟钱过不去?N个月之前,公司 ...
- 基于netty轻量的高性能分布式RPC服务框架forest<下篇>
基于netty轻量的高性能分布式RPC服务框架forest<上篇> 文章已经简单介绍了forest的快速入门,本文旨在介绍forest用户指南. 基本介绍 Forest是一套基于java开 ...
- 基于netty轻量的高性能分布式RPC服务框架forest<上篇>
工作几年,用过不不少RPC框架,也算是读过一些RPC源码.之前也撸过几次RPC框架,但是不断的被自己否定,最近终于又撸了一个,希望能够不断迭代出自己喜欢的样子. 顺便也记录一下撸RPC的过程,一来作为 ...
- RSF 分布式 RPC 服务框架的分层设计
RSF 是个什么东西? 一个高可用.高性能.轻量级的分布式服务框架.支持容灾.负载均衡.集群.一个典型的应用场景是,将同一个服务部署在多个Server上提供 request.response 消息通知 ...
- RPC服务框架dubbo(一):简介和原理解析
前置概念 在学习dubbo前,需要先了解SOA和RPC这两个概念. SOA 1.英文名称(Service Oriented Ambiguity) 2.中文名称:面向服务架构 2.1 有一个专门提供服务 ...
- Thrift总结(二)创建RPC服务
前面介绍了thrift 基础的东西,怎么写thrift 语法规范编写脚本,如何生成相关的语言的接口.不清楚的可以看这个<Thrift总结(一)介绍>.做好之前的准备工作以后,下面就开始如何 ...
- NET Core微服务之路:自己动手实现Rpc服务框架,基于DotEasy.Rpc服务框架的介绍和集成
本篇内容属于非实用性(拿来即用)介绍,如对框架设计没兴趣的朋友,请略过. 快一个月没有写博文了,最近忙着两件事; 一:阅读刘墉先生的<说话的魅力>,以一种微妙的,你我大家都会经常遇见 ...
- 唯品会RPC服务框架与容器化演进--转
原文地址:http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=405781868&idx=1&sn=cbb10d37e25 ...
- Thrift 个人实战--Thrift 服务化 Client的改造
前言: Thrift作为Facebook开源的RPC框架, 通过IDL中间语言, 并借助代码生成引擎生成各种主流语言的rpc框架服务端/客户端代码. 不过Thrift的实现, 简单使用离实际生产环境还 ...
随机推荐
- jdbc中如何实现模糊查询
情况如何 再利用jdbc执行sql语句的时候,对于其他的句子的执行没什么太大的问题:加上占位符,然后设置占位符的值. 但是在模糊查询的时候,一直都写不对,这里提供了两种可选的解决办法,以供参考. 解决 ...
- Scrum Meeting 1-20151201
任务安排 姓名 今日任务 明日任务 困难 董元财 学习下拉刷新的实现 完成下拉刷新的实现 手机的点击动作长按和下拉有类似的地方,比较难解决 胡亚坤 学习圆形头像代码设计 完成圆形头像代码设计 无 刘猛 ...
- io流操作大全
JAVA 中的IO流 一.流的概念 流(stream)的概念源于UNIX中管道(pipe)的概念.在UNIX中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备.外部 ...
- Mac下安装Wireshark,双击闪退
Mac OS X上使用Wireshark抓包(http://blog.csdn.net/phunxm/article/details/38590561) Mac下安装Wireshark /Appli ...
- SQL Server去掉字段内的双引号
今天在客户处遇到一个问题,用powershell抓取出的数据插入SQL中后每个字段都会自动带双引号“”如下: 现在想将此双引号去掉,用下面语句即可: insert into #A select SUB ...
- express-generator安装时出错,最后用VPS解决
npm install -g express-generator npm ERR! Linux 3.10.0-229.el7.x86_64npm ERR! argv "/usr/local/ ...
- Chinese culture
文房四宝 笔墨纸砚是中国古代文人书房中必备的宝贝,被称为“文房四宝”.用笔墨书写绘画在 中国可追溯到五千年前.秦(前221---前206)时已用不同硬度的毛和竹管制笔:汉代(前206—公元220) ...
- ToolStrip添加自定义的DateTimePicker
直接新建一个类,代码如下: [ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.All), DefaultEven ...
- 在Ubuntu中创建与Windows的共享文件夹
在Ubuntu中创建与Windows的共享文件夹 本周的学习需要用到一个压缩包,我从QQ群里下载到了Windows里.虽说我已经安装了虚拟机增强功能,能够与Windows系统共享剪切板,但它也只能共享 ...
- ABAP字符串翻转
就这个函数STRING_REVERSE 略显蛋疼,好搞那么复杂.... 简单的转换嘛: FUNCTION ZSTRING_REVERSE. *"----------------------- ...