基于docker部署的微服务架构(八): 日志数据保存到MongoDB数据库

前言

上一篇 基于docker部署的微服务架构(七): 部署ELK日志统计分析系统 中,已经把日志数据输出到 elasticsearch 并用 kibana 做展现。
实际项目中还有把日志数据保存到数据库,做进一步分析的需求。由于数据分析的需求有可能变动(例如:可能一开始只需要统计服务使用总量,后来随着业务的扩展,需要按不同地区或不同服务进行统计),保存日志的数据结构也会相应的变动,如果使用传统的关系型数据库,就需要对表结构进行修改,一般日志的数据量会很大,修改表结构很耗时,对开发也不友好,这时候应该考虑 nosql 数据库。
nosql 的使用情况来看,MongoDB 是使用最多的,一方面由于良好的性能,尤其在收购了 wiredtiger 引擎之后,提供了文档级锁,写入性能大大提高;另一方面就是 MongoDB 比较容易上手,hbase 依赖 hadoop 环境,部署起来比较麻烦,MongoDB 就很简单了。

本文将会介绍,在 docker 环境下搭建 MongoDB,搭建 MongoDB 的web客户端 mongo-expressMongoDB 简单使用,使用 spring kafka 创建 kafka 消费者接受消息,以及使用 spring data 操作 MongoDB

MongoDB的优缺点

先说优点,nosql 数据库的优点, MongoDB 都具备:高扩展性、高性能、松散的数据结构、天然支持分片和集群等等。
MongoDB 在此之上还提供了非常丰富的查询功能,不像 hbase 只能全表或 row key 查询。MongoDB 还提供了二级索引,并且还支持 MapReduceMongoDB 还提供了一个分布式文件系统 GridFS,用来存储超过16MB的数据。
缺点,不支持事务,这是 nosql 数据库的通病,也是 nosql 的基因所致。nosql 从诞生就 不是为了 处理结构化的强一致性数据的。
像日志数据这种,结构松散、不要求一致性、数据量大的数据,保存到 MongoDB 是个不错的选择。

在docker环境中部署MongoDB

登录 docker 节点,运行 docker pull mongo:3.2.11 下载目前最新的 MongoDB 镜像(建议在拉取镜像时使用具体的版本号,不要用 latest,避免版本兼容的问题,也更清楚具体用的哪个版本)。
创建数据挂载卷,mkdir -p /mongodb/data
启动容器

docker run -d --name mongodb --volume /mongodb/data:/data/db \
--publish 27017:27017 \
mongo:3.2.11

运行 docker exec -it mongodb mongo 进入 Mongo shell,运行 show dbs 查看当前所有的数据库。

简单介绍下 MongoDB 的基础概念:

  • database 和 mysql 类似,表示数据库
  • collection 相当于 mysql 中的表,用来存放数据,不同的是 collection 不需要定义表结构
  • document 相当于 mysql 表中保存的一条数据,BSON格式,BSON类似于JSON

Mongo shell 中运行 use add-service-demo-log,创建一个 add-service-demo-log 数据库,并切换到了这个数据库。
这时候运行 show dbs 并没有显示 add-service-demo-log 数据库,因为新建的这个数据库中没有 collection,新建一个 collectiondb.createCollection('addLog')
再运行 show dbs 就可以看到 add-service-demo-log 数据库了。
运行 db,查看当前处于哪个数据库。
运行 show collections 查看数据库中所有的 collection

在docker环境中部署mongo-express

在使用 MongoDB 开发时,通常需要一个客户端工具帮助我们操作 MongoDB,有很多优秀的客户端工具可以选择。这里我们部署一个web端的客户端工具 mongo-express,web工具的好处就是只要部署在服务端,所有开发人员都可以使用。
运行 docker pull mongo-express:0.32.0,下载镜像。
启动容器

docker run -d --name mongo-express --link mongodb:mongo \
--publish 8081:8081 \
mongo-express:0.32.0

--link 的时候给 mongodb 一个别名:mongo,因为 mongo-express 默认的MongoDB server是 mongo,这样的话就不用指定 ME_CONFIG_MONGODB_SERVER 环境变量了。

启动完成之后访问 http://宿主机IP:8081,打开 mongo-express 的页面。

基于docker部署的微服务架构(八): 日志数据保存到MongoDB数据库_第1张图片
mongo-express

修改add-service-demo把日志发送到kafka的add-log topic

修改 log4j2.xml 配置文件,新增一个 kafka appender ,日志的输出格式使用 JsonLayout

    
        
        ${kafkaBootstrapServers}
    

把日志数据输出到 kafkaadd-log topic 下。对这个 appender 进行异步包装:

    
        
    

最后增加一个 Logger,使用之前配置的异步 appender

    
        
    

修改完之后 log4j2.xml 的文件内容:

  
  
      
          
              %d{yyyy-MM-dd HH:mm:ss.SSS}{GMT+8} [@project.artifactId@] [%thread] %-5level %logger{35} - %msg %n
          
          
              @kafka.bootstrap.servers@
          
      
      
          
              
          

          
              
                  
                      ${logFormat}
                  
              
              
                  
              
          

          
              
                  
                      ${logFormat}
                  
              
              ${kafkaBootstrapServers}
          

          
              
              ${kafkaBootstrapServers}
          

          
              
          

          
              
          

          
              
          
      
      
          
          
              
          
          
              
              
              
          
      
  

AddController.java 中使用 addLogger 输出日志:

  @RestController
  @RefreshScope
  public class AddController {
      private static final Logger addLogger = LoggerFactory.getLogger("addLogger");

      @Value("${my.info.str}")
      private String infoStr;

      @RequestMapping(value = "/add", method = RequestMethod.GET)
      public Map add(Integer a, Integer b) {
          System.out.println("端口为8100的实例被调用");
          System.out.println("infoStr : " + infoStr);
          Map returnMap = new HashMap<>();
          returnMap.put("code", 200);
          returnMap.put("msg", "操作成功");
          Integer result = a + b;
          returnMap.put("result", result);

          addLogger.info("a : " + a + ", b : " + b + ", a + b :" + result);
          return returnMap;
      }

  }

这样在调用 AddController 中的 add 方法时,会输出一条日志到 kafkaadd-log topic 中,只要创建一个消费者订阅 add-log,把日志数据保存到 MongoDB 即可。

创建日志消费者

新建一个 maven 项目,修改 pom.xml 增加需要的依赖:


    org.springframework.boot
    spring-boot-starter-parent
    1.4.2.RELEASE



    
        org.springframework.boot
        spring-boot-starter
        
            
                org.springframework.boot
                spring-boot-starter-logging
            
        
    
    
        org.springframework.boot
        spring-boot-starter-log4j2
    

    
        org.apache.kafka
        kafka-clients
        0.10.0.1
    
    
        org.springframework.cloud
        spring-cloud-starter-eureka
    
    
        org.springframework.cloud
        spring-cloud-starter-config
    

    
        org.springframework.cloud
        spring-cloud-starter-bus-amqp
    

    
        org.springframework.kafka
        spring-kafka
        1.1.1.RELEASE
    

    
        com.fasterxml.jackson.core
        jackson-core
    

    
        com.fasterxml.jackson.core
        jackson-databind
    

    
        org.springframework.boot
        spring-boot-starter-data-mongodb
    



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            Camden.SR2
            pom
            import
        
    



    
    1.8
    
    
        registry.cn-hangzhou.aliyuncs.com/ztecs
    
    
    demo

    
    

    10.47.160.238:9092



    
    
        docker

        
            docker
            docker-demo-${project.version}

            kafka:9092
        
    



    install
    ${project.artifactId}

    
        
            src/main/resources
            true
        
    

    
        
        
            org.springframework.boot
            spring-boot-maven-plugin
            
                true
            
        

        
        
            org.apache.maven.plugins
            maven-surefire-plugin
            
                true
            
        

        
        
            com.spotify
            docker-maven-plugin
            0.4.13
            
                
                    install
                    
                        build
                        tag
                    
                
            
            
                
                http://docker节点ip:2375
                ${docker.image.prefix}/${project.build.finalName}
                java
                
                ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/${project.build.finalName}.jar"]
                
                
                    
                        /
                        ${project.build.directory}
                        ${project.build.finalName}.jar
                    
                
                ${docker.image.prefix}/${project.build.finalName}
                ${docker.image.prefix}/${project.build.finalName}:${docker.tag}
                true
                false
            
        
    

看过之前文章的朋友应该对这些配置比较熟悉了,和其他项目不同的是,这里引入了 spring-kafkajacksonspring-boot-starter-data-mongodb
使用 spring-kafka 创建 kafka 订阅者,使用 jackson 对日志数据进行转换,使用 spring-boot-starter-data-mongodb 完成 MongoDB 的相关操作。

resources 目录下创建 bootstrap.yml,因为配置信息是从 config-server 中获取的,所以 bootstrap.yml 的内容和其他项目一样:

  spring:
    application:
      name: @project.artifactId@
    profiles:
      active: @activatedProperties@
    cloud:
      config:
        profile: dev
        label: master
        discovery:
          enabled: true
          serviceId: CONFIG-SERVER-DEMO
        failFast: true
        retry:
          initialInterval: 10000
          multiplier: 2
          maxInterval: 60000
          maxAttempts: 10

  eureka:
    client:
      serviceUrl:
        defaultZone: http://localhost:8000/eureka/

在git仓库中创建配置文件 log-persist-demo-dev.yml ,:

  spring:
    rabbitmq:
      host: 10.47.160.238
      port: 5673
      username: guest
      password: guest
    data:
      mongodb:
        uri: mongodb://10.47.160.114:27017/add-service-demo-log

  kafka:
    bootstrapServers: 10.47.160.114:9092
    groupId: mongo
    enableAutoCommit: true
    autoCommitIntervalMs: 100
    sessionTimeOutMs: 15000

创建 log4j2.xml 配置文件,内容也和其他项目相同:

  
  
      
          
              %d{yyyy-MM-dd HH:mm:ss.SSS}{GMT+8} [@project.artifactId@] [%thread] %-5level %logger{35} - %msg %n
          
          
              @kafka.bootstrap.servers@
          
      
      
          
              
          

          
              
                  
                      ${logFormat}
                  
              
              
                  
              
          

          
              
                  
                      ${logFormat}
                  
              
              ${kafkaBootstrapServers}
          

          
              
          

          
              
          

      
      
          
          
              
              
              
          
      
  

创建一个 demo 包,在 demo 包下创建启动入口 LogPersistDemoApplication.java

        @SpringBootApplication
        public class LogPersistDemoApplication {

            public static void main(String[] args) {
                SpringApplication.run(LogPersistDemoApplication.class, args);
            }

        }

demo 下创建一个子包 config,用来存放 java config 配置。创建 KafkaConfig.java 配置 kafka

  @Configuration
  @EnableKafka
  public class KafkaConfig {

      @Value("${kafka.bootstrapServers}")
      private String bootstrapServers;

      @Value("${kafka.groupId}")
      private String groupId;

      @Value("${kafka.enableAutoCommit}")
      private Boolean enableAutoCommit;

      @Value("${kafka.autoCommitIntervalMs}")
      private Integer autoCommitIntervalMs;

      @Value("${kafka.sessionTimeOutMs}")
      private Integer sessionTimeOutMs;

      @Bean
      ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
          ConcurrentKafkaListenerContainerFactory factory =
                  new ConcurrentKafkaListenerContainerFactory<>();
          factory.setConsumerFactory(consumerFactory());
          return factory;
      }

      @Bean
      public ConsumerFactory consumerFactory() {
          return new DefaultKafkaConsumerFactory<>(consumerConfigs());
      }

      @Bean
      public Map consumerConfigs() {
          Map props = new HashMap<>();
          props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
          props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
          props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
          props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs);
          props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeOutMs);
          props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
          props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
          return props;
      }
  }

简单看下 KafkaConfig.java ,在类级别使用 @EnableKafkaspring boot 自动帮我们初始化 kafka 相关的bean。配置类中用到的配置信息使用 @Valuelog-persist-demo-dev.yml 读取数据,并配置了 kafkaListenerContainerFactory,用来创建 kafka 消费者。

config 包下创建 MongoConfig.java 配置 MongoDB

  @Configuration
  public class MongoConfig {

      @Value("${spring.data.mongodb.uri}")
      private String mongoClientUri;

      @Bean
      public MongoDbFactory mongoDbFactory() {
          SimpleMongoDbFactory mongoDbFactory = null;
          try {
              mongoDbFactory = new SimpleMongoDbFactory(new MongoClientURI(mongoClientUri));
              mongoDbFactory.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
          } catch (UnknownHostException e) {
              e.printStackTrace();
          }
          return mongoDbFactory;
      }
  }

这里配置了 MongoClientURI,并且设置了写安全级别(WriteConcern)为 UNACKNOWLEDGED,关于 MongoDB 写安全级别,简单介绍:

  • Errors Ignored(-1) 忽略所有异常,包括网络异常。
  • Unacknowledged(0) 忽略写入异常,但是会检测网络异常。
  • Acknowledged(1) 默认级别,可以捕获到写入异常。 MongoDB 保存数据时,先把数据写入内存,定期 fsync 保存到硬盘,如果数据写入内存之后没来得及写入硬盘,服务挂了,数据就丢了。
  • Journaled(1, journal=true) 增加 journal 日志,数据写入内存的同时记录日志,服务down了可以通过 journal 日志还原操作
  • majority(>1) 在副本集模式下,保证多数节点(超过半数)数据写入。

从上到下,安全级别由低到高,写入效率由高到低。由于记录的是日志数据,数据量大,对写入效率要求较高,并且允许部分数据丢失,所以配置了 Unacknowledged 级别,最低级别会忽略网络异常,一般不建议使用。

demo 包下创建一个 model 子包,用来存放数据模型。创建 AddLog.java 数据模型:

  public class AddLog {
      @Id
      private String id;

      private Long timeMillis;
      private String thread;
      private String level;
      private String loggerName;
      private String message;
      private Boolean endOfBatch;
      private String loggerFqcn;
      private Integer threadId;
      private Integer threadPriority;

      public String getId() {
          return id;
      }

      public void setId(String id) {
          this.id = id;
      }

      public Long getTimeMillis() {
          return timeMillis;
      }

      public void setTimeMillis(Long timeMillis) {
          this.timeMillis = timeMillis;
      }

      public String getThread() {
          return thread;
      }

      public void setThread(String thread) {
          this.thread = thread;
      }

      public String getLevel() {
          return level;
      }

      public void setLevel(String level) {
          this.level = level;
      }

      public String getLoggerName() {
          return loggerName;
      }

      public void setLoggerName(String loggerName) {
          this.loggerName = loggerName;
      }

      public String getMessage() {
          return message;
      }

      public void setMessage(String message) {
          this.message = message;
      }

      public Boolean getEndOfBatch() {
          return endOfBatch;
      }

      public void setEndOfBatch(Boolean endOfBatch) {
          this.endOfBatch = endOfBatch;
      }

      public String getLoggerFqcn() {
          return loggerFqcn;
      }

      public void setLoggerFqcn(String loggerFqcn) {
          this.loggerFqcn = loggerFqcn;
      }

      public Integer getThreadId() {
          return threadId;
      }

      public void setThreadId(Integer threadId) {
          this.threadId = threadId;
      }

      public Integer getThreadPriority() {
          return threadPriority;
      }

      public void setThreadPriority(Integer threadPriority) {
          this.threadPriority = threadPriority;
      }

  }

创建数据访问层 AddLogDao.dao,使用 spring dataMongoDB 的封装 :

  @Repository
  public interface AddLogDao extends MongoRepository {
  }

创建 KafkaConsumer.java 接受 kafka 消息:

@Component
public class KafkaConsumer {

    private static final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);


    @Autowired
    private AddLogDao addLogDao;

    @KafkaListener(topics = {"add-log"})
    public void receivePersistLog(String data) {
        logger.info("接收到需要保存到MongoDB的日志数据, data : " + data);
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            AddLog addLog = objectMapper.readValue(data, AddLog.class);
            addLogDao.save(addLog);
            logger.info("成功保存日志数据, data : " + data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里使用 @KafkaListener(topics = {"add-log"}) 接受 add-log topic 的消息,把收到的消息保存到 MongoDB

运行 LogPersistDemoApplication.javamain 方法启动,访问 add-service-demo 提供的 add 接口,会在 MongoDB 中插入一条日志记录。

使用docker-maven-plugin打包并生成docker镜像

这部分内容和前面几篇文章基本相同,都是把容器间的访问地址和 --link 参数对应,不再赘述。

demo源码 spring-cloud-3.0目录

最后

本文简单介绍了 MongoDB 的相关内容, 以及使用 spring kafka 接受kafka消息,并把数据插入 MongoDB

你可能感兴趣的:(基于docker部署的微服务架构(八): 日志数据保存到MongoDB数据库)