1. 大纲
2. 业务背景(situation)
消息网关内部采用MySQL
进行消息持久化,需要大量的I/O开销
,因此在高并发请求下会成为系统瓶颈,亟需一种高吞吐量的替代方案。
这里主要思考了2种解决方案:
-
寻找一种
MySQL
的替代方案。由于MySQL
是基于B-Tree
的,考虑性能提升的话,需要采用基于LSM-Tree
的方案设计的数据库- 但是这种方案涉及业务侧比较大的改造(对于当前MVC3层结构的代码来说,因为并未对repo层进行抽象,因此替换底层存储几乎是革命性的变革)
B-Tree
vsLSM-Tree
,分别适合读多和写多的场景
-
放弃以
DB
进行数据持久化的方案,转而采用ES
等其他引擎。这里又可以进一步细化为2种方式,分别为- 代码中直接嵌入
ES-Template
,将数据存储到ES
中 - 将数据写入
log
中,通过中间件将log
中的信息同步至ES
其中,第一种方案需要引入新的依赖,同时在有新租户接入时面临比较大的代码编写任务;而第二种方案仅需配置
logback.xml
,在有新tenant接入时,采用扩展的方式就可以很好的完成对接。 - 代码中直接嵌入
3. 设计思路(task)
这里需要着重考虑的一点是,写入不同的log文件时,是否可以采用对先用代码无侵入的解决方案?
答案是:javaagent
4. 重点难点(action)
4.1 logback
- 系统引入logback.jar依赖
- 编写logback.xml文件
- 日志存储位置LOG_DIR
- 日志输出格式pattern
- 多个日志appender
- 异步日志打印ASYNC
- 日志类配置logger name
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定义日志文件的存储地址 -->
<property name="LOG_DIR" value="resource/log-save"/>
<!--
%p:输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r:输出自应用启动到输出该日志讯息所耗费的毫秒数
%t:输出产生该日志事件的线程名
%f:输出日志讯息所属的类别的类别名
%c:输出日志讯息所属的类的全名
%d:输出日志时间点的日期或时间,指定格式的方式: %d{yyyy-MM-dd HH:mm:ss}
%l:输出日志事件的发生位置,即输出日志讯息的语句在他所在类别的第几行。
%m:输出代码中指定的讯息,如log(message)中的message
%n:输出一个换行符号
-->
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %msg%n"/>
<!--
Appender: 设置日志信息的去向,常用的有以下几个
ch.qos.logback.core.ConsoleAppender (控制台)
ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
ch.qos.logback.core.FileAppender (文件)
-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 字符串System.out(默认)或者System.err -->
<target>System.out</target>
<!-- 对记录事件进行格式化 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<appender name="tenant_A" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/tenantA.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 必要节点,包含文件名及"%d"转换符,"%d"可以包含一个java.text.SimpleDateFormat指定的时间格式,默认格式是 yyyy-MM-dd -->
<fileNamePattern>${LOG_DIR}/tenantA_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,如果是6,则只保存最近6个月的文件,删除之前的旧文件 -->
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!-- LevelFilter: 级别过滤器,根据日志级别进行过滤 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<!-- 用于配置符合过滤条件的操作 ACCEPT:日志会被立即处理,不再经过剩余过滤器 -->
<onMatch>ACCEPT</onMatch>
<!-- 用于配置不符合过滤条件的操作 DENY:日志将立即被抛弃不再经过其他过滤器 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="tenant_B" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/tenantB.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 必要节点,包含文件名及"%d"转换符,"%d"可以包含一个java.text.SimpleDateFormat指定的时间格式,默认格式是 yyyy-MM-dd -->
<fileNamePattern>${LOG_DIR}/tenantB_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,如果是6,则只保存最近6个月的文件,删除之前的旧文件 -->
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!-- LevelFilter: 级别过滤器,根据日志级别进行过滤 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<!-- 用于配置符合过滤条件的操作 ACCEPT:日志会被立即处理,不再经过剩余过滤器 -->
<onMatch>ACCEPT</onMatch>
<!-- 用于配置不符合过滤条件的操作 DENY:日志将立即被抛弃不再经过其他过滤器 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="tenant_Default" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/tenantDefault.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 必要节点,包含文件名及"%d"转换符,"%d"可以包含一个java.text.SimpleDateFormat指定的时间格式,默认格式是 yyyy-MM-dd -->
<fileNamePattern>${LOG_DIR}/tenantDefault_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,如果是6,则只保存最近6个月的文件,删除之前的旧文件 -->
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!-- LevelFilter: 级别过滤器,根据日志级别进行过滤 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<!-- 用于配置符合过滤条件的操作 ACCEPT:日志会被立即处理,不再经过剩余过滤器 -->
<onMatch>ACCEPT</onMatch>
<!-- 用于配置不符合过滤条件的操作 DENY:日志将立即被抛弃不再经过其他过滤器 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 文件 异步日志(async) -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender" >
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<neverBlock>true</neverBlock>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="tenant_A" />
<appender-ref ref="tenant_B" />
<appender-ref ref="tenant_Default" />
</appender>
<!--
也是<logger>元素,但是它是根logger。默认debug
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
<root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger。
-->
<root level="info">
<level>info</level>
<!-- <appender-ref ref="STDOUT"/>-->
<appender-ref ref="ASYNC"/>
<appender-ref ref="tenant_A"/>
<appender-ref ref="tenant_B"/>
<appender-ref ref="tenant_Default"/>
</root>
<logger name="com.example.logback.domain.factory.DefaultLogger" level="warn" additivity="false">
<level value="warn"/>
<appender-ref ref="tenant_Default"/>
</logger>
<logger name="com.example.logback.domain.factory.TenantALogger" level="warn" additivity="false">
<level value="warn"/>
<appender-ref ref="tenant_A"/>
</logger>
<logger name="com.example.logback.domain.factory.TenantBLogger" level="warn" additivity="false">
<level value="warn"/>
<appender-ref ref="tenant_B"/>
</logger>
</configuration>
4.2 DDD
4.2.1 编写消息写入log的代码
- DDD层级划分
- 代码层级划分
- UML图
- UML类图知识点回顾
- 强弱关系:依赖 < 关联 < 聚合 < 组合
- 依赖
- 表示方式:虚线箭头
- 解释说明:对象A作为对象B方法的一个参数,则对象B依赖于对象A
- 关联
- 表示方式:实线箭头
- 解释说明:对象A作为对象B的一个属性,则对象B依赖于对象A
- 聚合
- 表示方式:空心菱形加实线
- 解释说明:弱的拥有关系,has a的一种情形,两者不需要有相同的生命周期
- 组合
- 表示方式:实心菱形加实线
- 解释说明:强的拥有关系,contains a的一种情形,两者是严格的整体与部分的关系
- 代码说明
- 入口是fileController中的logSave和logEventSave,其中logSave方法用于模拟正常的日志存储、logEventSave方法用来模拟消息送达后的事件触发日志存储。
- fileController中的传参分为三种类型,分别是commend、query、event。分别对应于写请求、读请求和事件请求。
- 事件请求是指,将原本串行化执行的指令修改为监听事件触发。在Spring中可以直接使用Spring Event机制。该机制通过编写ApplicationEvent、ApplicationListener并交由ApplicationEventPublisher进行事件发布,完成全部流程。使用监听器模式处理事件请求可以很好的实现逻辑解耦,以遵循单一职责原则。
- 具体Logger对象实例的构造,采用了策略模式实现,通过传递参数中的属性,在LoggerPolicyContext中进行判断后构造。
- UML类图知识点回顾
4.3 javaagent
这部分很多,参考我的另一篇文章:
https://www.yuque.com/docs/share/205ed300-cb08-4929-8cb4-7d61631fd152?# 《2022-02-18【agent代理】》
4.4 filebeat接入
一波三折的一次实践。
首先,晒出最终的filebeat
配置:
###################### Filebeat Configuration Example #########################
# This file is an example configuration file highlighting only the most common
# options. The filebeat.full.yml file from the same directory contains all the
# supported options with more comments. You can use it as a reference.
#
# You can find the full configuration reference here:
# https://www.elastic.co/guide/en/beats/filebeat/index.html
#=========================== Filebeat prospectors =============================
filebeat.inputs:
# Each - is a prospector. Most options can be set at the prospector level, so
# you can use different prospectors for various configurations.
# Below are the prospector specific configurations.
- type: log
enabled: true
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /home/admin/koms/log2/error.log
json.keys_under_root: true
json.overwrite_keys: true
tags: ["error"]
- type: log
enabled: true
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /home/logback/resource/log-save/tenantA.log
json.keys_under_root: true
json.overwrite_keys: true
tags: ["tenantA"]
- type: log
enabled: true
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /home/logback/resource/log-save/tenantB.log
json.keys_under_root: true
json.overwrite_keys: true
tags: ["tenantB"]
- type: log
enabled: true
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /home/logback/resource/log-save/tenantDefault.log
json.keys_under_root: true
json.overwrite_keys: true
tags: ["tenantDefault"]
# - /home/admin/koms/log2/info.log
#- c:\programdata\elasticsearch\logs\*
# Exclude lines. A list of regular expressions to match. It drops the lines that are
# matching any regular expression from the list.
#exclude_lines: ["^DBG"]
# Include lines. A list of regular expressions to match. It exports the lines that are
# matching any regular expression from the list.
#include_lines: ["^ERR", "^WARN", "^INFO"]
# Exclude files. A list of regular expressions to match. Filebeat drops the files that
# are matching any regular expression from the list. By default, no files are dropped.
#exclude_files: [".gz$"]
# Optional additional fields. These field can be freely picked
# to add additional information to the crawled log files for filtering
#fields:
# level: debug
# review: 1
### Multiline options
# Mutiline can be used for log messages spanning multiple lines. This is common
# for Java Stack Traces or C-Line Continuation
# The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
# multiline.pattern: '^\[[0-9]{4}-[0-9]{2}-[0-9]{2}'
# Defines if the pattern set under pattern should be negated or not. Default is false.
# multiline.negate: true
# Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
# that was (not) matched before or after or as long as a pattern is not matched based on negate.
# Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
# multiline.match: after
# multiline.max_lines: 2000
#================================ General =====================================
# The name of the shipper that publishes the network data. It can be used to group
# all the transactions sent by a single shipper in the web interface.
#name:
# The tags of the shipper are included in their own field with each
# transaction published.
#tags: ["service-X", "web-tier"]
# Optional fields that you can specify to add additional information to the
# output.
#fields:
# env: staging
#================================ Outputs =====================================
# Configure what outputs to use when sending the data collected by the beat.
# Multiple outputs may be used.
#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
# Array of hosts to connect to.
hosts: ["xxxx:9200"]
indices:
- index: "filebeat-error-%{+yyyy.MM.dd}"
when.contains:
tags: "error"
- index: "tenanta-%{+yyyy.MM.dd}"
when.contains:
tags: "tenantA"
- index: "tenantb-%{+yyyy.MM.dd}"
when.contains:
tags: "tenantB"
- index: "tenantdefault-%{+yyyy.MM.dd}"
when.contains:
tags: "tenantDefault"
# Optional protocol and basic auth credentials.
#protocol: "https"
#username: "elastic"
#password: "123456"
#----------------------------- Logstash output --------------------------------
#output.logstash:
# The Logstash hosts
#hosts: ["localhost:5044"]
# Optional SSL. By default is off.
# List of root certificates for HTTPS server verifications
#ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]
# Certificate for SSL client authentication
#ssl.certificate: "/etc/pki/client/cert.pem"
# Client Certificate Key
#ssl.key: "/etc/pki/client/cert.key"
#================================ Logging =====================================
# Sets log level. The default log level is info.
# Available log levels are: critical, error, warning, info, debug
#logging.level: debug
# At debug level, you can selectively enable logging only for some components.
# To enable all selectors use ["*"]. Examples of other selectors are "beat",
# "publish", "service".
#logging.selectors: ["*"]
一开始启动时一直报错下面这个错误,检索到的资料都说是配置文件写法问题。
Exiting: No modules or prospectors enabled and configuration reloading disabled. What files do you want me to watch?
最终发现,并不是写法问题,而是由于filebeat版本过低导致的。上面的写法需要filebeat-6.x,而测试环境使用的还是5.x版本。
果断升级了新版本后,启动成功。
在filebeat向es创建索引的过程中,还出现了一些问题,那就是es中的索引不能有大写字母,所以修改了一下配置文件的index字段信息。
按照上述方法得到的es查询日志如下,不是很直观,因此需要format后进行便捷的查询
4.5 ES同步
- filebeat将数据同步至es中
- es层面的查询,采用kibana提供的sense组件实现