• storm实践


    一 环境与部署

    1. 环境变量

      由于是分布式环境,对于java单机的 -Djvm参数不适用,在storm中处理环境变量是通过org.apache.storm.Config,在提交任务创建Topology时设置Config,在Spout和Bolt中通过open方法中的config参数读取

    2. 部署

      storm集群重启时会有短暂时间的不可用,解决这种方式可以通过部署两个功能完全相同的集群,在提交任务的时候传入集群名,就好像同样的服务部署在多个机器上,但是这种方式对于按照字段分组的场景可能产生并发问题

    以下为启动代码示例:

    public class TestTopology {
    
        private static final Logger logger = LoggerFactory.getLogger(TestTopology.class);
    
        protected static String topologyIndex = ""; // 区分A,B集群
    
        protected String testTopology = "test-topology";
    
        public static void main(String args[]) {
            topologyIndex = args[0];
            if (args.length > 1) {
                System.setProperty("env", args[1]); // 接收环境变量
                System.out.println("init env is " + args[1]);
            }
            if (StringUtils.isEmpty(topologyIndex)) {
                logger.error("topology index error");
                System.exit(0);
            }
            new TestTopology().build();
        }
    
        public void build() {
            AdStormEnvironment.init(System.getProperties(), false);
            TopologyBuilder builder = new TopologyBuilder();
    
    
            Config config = new Config();
            config.put("env", System.getProperty("env")); // 设置环境变量
            try {
                StormSubmitter.submitTopology(testTopology + "-" + topologyIndex, config, builder.createTopology());
            } catch (Exception e) {
                SlfLogUtil.error(logger, e, "部署集群任务失败|{0}|{1}", System.getenv(), StormEnvironment.getName(testTopology));
            }
        }
    }

    二 日志兼容问题

      storm日志可以说是最头疼的问题,这也和java中日志体系有关,log4j log4j2 slf4j commons-logging jdk-log logback 等等

      java日志体系分成两类,一类是实现,一类是接口定义。实现就是具体的log,接口定义主要是为了整合其他日志框架

    1. 日志类实现

      jdk-logging, jdk自带log类,基本没人用

      log4j, log4j的实现,常见版本1.2.17

      logback,log4j作者的有一个实现,性能更好

      log4j2,性能更好的日志实现,storm使用的就是log4j2,版本为2.8。只不过log4j2引入有两个包,一个是log4j2-api,一个是log4j2-core,所以其他日志框架实现了log4j2-api都可以整合到log4j2的项目中

      commons-logging,由于log4j先于jdk-logging,commons-logging主要做的事就是优先使用log4j,没有的话使用jdk-logging,再不行的话使用自己的实现

    2. 日志类接口

      slf4j,一种日志标准实现的定义,逐渐形成新的行业标准

      slf4j有很多子包,所有xxx-slf4j-impl的包都是slf4j的具体实现,例如log4j-slf4j-impl, log4j2-slf4j-impl等

      实现原理就是调用具体的日志实现的方法,比如log4j-slf4j-impl中的Logger会调用log4j的Logger

      xxx-over-slf4j的包都是把日志实现转移到slf4j上,例如log4j-over-slf4j

      实现原理是比如log4j包中有个叫Logger的类,在log4j-over-slf4j包中也有个同名的Logger类,但是方法实现是调用slf4j的api,java中同名类加载的顺序是按照classpath哪个在前面就先加载哪一个,如果是先加载了log4j也是无法生效的

      所以xxx-over-slf4j和xxx-slf4j-impl不能同时引入,不然会造成死循环

    3. storm中引入了哪些包

      slf4j-api log4j-api-2.8 log4j-slf4j-impl-2.8 log4j-core-2.8 这几个主要是slf4j和log4j2的组合

      log4j-over-slf4j-1.6.6 log4j的日志转到slf4j

    log4j-over-slf4j-1.6.6 中的坑,主要是这个adapter对于log4j-1.2.17的实现有问题,比如https://jira.qos.ch/browse/SLF4J-368 这个问题,在使用DailyRollingAppender时会有问题,还有一些奇奇怪怪的问题,总的来说还是要把log4j迁移到slf4j比较保险

      storm应用中会引入storm-core这个包,这个包也会依赖log4j2,当版本与storm安装目录下log4j2的2.8版本不一致的时候可能会产生错误,maven打包时最好execlude掉

    三. 日志配置问题

      由于storm不能指定log4j2配置文件的位置,所以集群中只能使用一份配置,对于测试和线上环境的区分就比较麻烦,以下给出解决方案如下:

      修改config.yaml

      worker.childopts: "-Xloggc:/data1/logs/storm/%TOPOLOGY-ID%/%ID%/gc.log" 其中%TOPOLOGY-ID%代表拓扑名,%ID%代表worker端口号,这样可以让GC LOG打印到与worker log同级

      修改worker.xml

    <configuration monitorInterval="60" shutdownHook="disable">
        <properties>
            <property name="pattern">[%p] %d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%M line:%L] %m%n</property>
            <property name="baseDir">/data1/logs/storm</property>
        </properties>
        <appenders>
            <RollingFile name="ROOT_LOG"
                         fileName="${baseDir}/${sys:storm.id}/${sys:worker.port}/root.log"
                         filePattern="${baseDir}/${sys:storm.id}/${sys:worker.port}/root.log.%d{yyyy-MM-dd}.gz">
                <PatternLayout>
                    <pattern>${pattern}</pattern>
                </PatternLayout>
                <Policies>
                    <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
                </Policies>
                <DefaultRolloverStrategy max="9"/>
            </RollingFile>
            <RollingFile name="worker"
                         fileName="${baseDir}/${sys:storm.id}/${sys:worker.port}/worker.log"
                         filePattern="${baseDir}/${sys:storm.id}/${sys:worker.port}/worker.log.%d{yyyy-MM-dd}.gz">
                <PatternLayout>
                    <pattern>${pattern}</pattern>
                </PatternLayout>
                <Policies>
                    <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
                </Policies>
                <DefaultRolloverStrategy max="9"/>
            </RollingFile>
            <RollingFile name="ERROR_LOG"
                         fileName="${baseDir}/${sys:storm.id}/${sys:worker.port}/stdout.log"
                         filePattern="${baseDir}/${sys:storm.id}/${sys:worker.port}/stdout.log.%d{yyyy-MM-dd}.gz">
                <PatternLayout>
                    <pattern>${pattern}</pattern>
                </PatternLayout>
                <Filters>
                    <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY" />
                </Filters>
                <Policies>
                    <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
                </Policies>
                <DefaultRolloverStrategy max="4"/>
            </RollingFile>
        </appenders>
        <loggers>
            <root level="info">
                <appender-ref ref="ROOT_LOG"/>
                <appender-ref ref="ERROR_LOG"/>
            </root>
            <Logger name="STDERR" level="INFO" additivity="true">
            </Logger>
            <Logger name="STDOUT" level="INFO" additivity="true">
            </Logger>
            <Logger name="oo.xx" level="info">
                <appender-ref ref="worker"/>
            </Logger>
        </loggers>
    </configuration>

      主要修改:

      a. 自己应用的日志打印到worker.log

        b. storm和一些其他方面的日志打印到root.log

          c. 所有error级别的log打印到stdout.log,用来监控应用错误

      d. [%c]-[%M line:%L] 这三个设置可以方便知道日志打印的 类,方法,行

      

      创建自己自定义的Logger类

    public class SlfLogUtil {
        
        private static final String FQCN = SlfLogUtil.class.getName();
    
        public static void info(Logger logger, String message) {
            if (logger instanceof LocationAwareLogger) {
                ((LocationAwareLogger) logger).log(null, FQCN, LocationAwareLogger.INFO_INT, message, null, null);
            } else {
                logger.info(message);
            }
        }
    
        public static void debug(Logger logger, String message, Object... params) {
            if (Environment.isTest()) {
                info(logger, format(message, params), false);
            }
        }
    }

      主要修改:

      a. 代理info debug error 方法,尤其是debug,自己可以根据环境变量来判断,这里Environment.isTest()方法可以自己实现

      b. 为了可以打印方法调用位置需要使用slf4j的LocationAwareLogger,不然日志中会变成SlfLogUtil方法调用的位置

    四. 日志收集问题

      Storm由于是分布式的,所以日志会分布在各各机器上,而且每次重启会新生成一次目录,非常不利于对应用日志的观察,提出如下几种解决办法

      可以使用日志收集客户端,直接采集文件,但是需要监控文件和文件夹的新增,比较麻烦

      还有一种办法是固定日志文件的名字,比如使用LOG4J2中的Routing功能,通过ThreadContext固定日志的位置,但是ThreadContext对于多线程的时候需要传递ThreadContext中的值,而且一个拓扑在不同worker中肯定无法输出到同一个文件,还是只能在固定路径生成stdout.$worker.log 这样的log

      第三种办法后来尝试直接使用KafkaAppender,通过网络转走日志,最后也是使用这种办法,如果使用Log4j2的KafkaAppender在配置文件中配置,测试环境和线上环境又不能在同一个topic,所以自己实现了KafkaAppender,具体实现如下

      

    @Plugin(name = "StormAppender", category = "Core", elementType = "appender", printObject = true)
    public class StormAppender extends AbstractAppender {
        
        String topic;
    
        Producer<String, String> producer = null;
    
        String hostName;
    
        /* 构造函数 */
        public StormAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions, String topic) {
            super(name, filter, layout, ignoreExceptions);
            this.topic = topic;
            producer = new KafkaProducer<String, String>(getKafkaConfig());
            try {
                hostName = InetAddress.getLocalHost().getHostName();
            } catch (Exception e) {
                hostName = "unknown";
            }
        }
    
        private Properties getKafkaConfig() {
            Properties props = new Properties();
            props.put("bootstrap.servers", "localhost:9092");
            props.put("acks", "1");
            props.put("retries", 2);
            props.put("batch.size", 512);
            props.put("linger.ms", 1);
            props.put("max.block.ms", 1000);
            props.put("buffer.memory", 32 * 1024 * 1024);
            props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
            props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
            props.put("compression.type", "gzip");
            return props;
        }
    
    
        @Override
        public void append(LogEvent event) {
            if (event.getLoggerName().startsWith("org.apache.kafka")) {
                LOGGER.warn("Recursive logging from [{}] for appender [{}].", event.getLoggerName(), getName());
            } else {
                try {
                    final Layout<? extends Serializable> layout = getLayout();
                    byte[] data;
                    if (layout != null) {
                        if (layout instanceof SerializedLayout) {
                            final byte[] header = layout.getHeader();
                            final byte[] body = layout.toByteArray(event);
                            data = new byte[header.length + body.length];
                            System.arraycopy(header, 0, data, 0, header.length);
                            System.arraycopy(body, 0, data, header.length, body.length);
                        } else {
                            data = layout.toByteArray(event);
                        }
                    } else {
                        data = StringEncoder.toBytes(event.getMessage().getFormattedMessage(), StandardCharsets.UTF_8);
                    }
                    String line = hostName + "|" + StormEnvironment.getTopology() + "|" + new String(data);
                    producer.send(new ProducerRecord<String, String>(topic, line));
                } catch (final Exception e) {
                    LOGGER.error("Unable to write to Kafka [{}] for appender [{}].", topic, getName(), e);
                    throw new AppenderLoggingException("Unable to write to Kafka in appender: " + e.getMessage(), e);
                }
            }
        }
    
        @PluginFactory
        public static StormAppender createAppender(@PluginAttribute("name") String name,
                                                   @PluginAttribute("topic") String topic,
                                                    @PluginElement("Filter") final Filter filter,
                                                    @PluginElement("Layout") Layout<? extends Serializable> layout,
                                                    @PluginAttribute("ignoreExceptions") boolean ignoreExceptions) {
            if (name == null) {
                LOGGER.error("no name defined in conf.");
                return null;
            }
            if (layout == null) {
                layout = PatternLayout.createDefaultLayout();
            }
            return new StormAppender(name, filter, layout, ignoreExceptions, topic);
        }
    
        @Override
        public void stop() {
    
        }
    }

      主要修改:

      a. 向kafka投递日志内容的时候会增加拓扑名,方便消费的时候做区分,根据LogEvent还可以做更多的定制

          b. 日志防止循环打印,如果是org.apache.kafka的日志不打印,还有Appender不要使用工程中的其他类,比如A类的m方法中记录日志会调用自定义的Appender,在自定义的Appender中又会调用A类的m方法造成死循环

      有了Appender,下一步是在启动拓扑的时候,将appender加入到rootlogger中,可以使用代码的方式动态更新rootlogger的appender,这样也可以根据环境来投递到不同的topic种,代码如下

    final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
    final Configuration config = ctx.getConfiguration();
    Layout layout = PatternLayout.newBuilder().withPattern("[%p] %d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%M line:%L] %m%n")
            .withConfiguration(config).build();
    for (Appender appender : config.getAppenders().values()) {
        if ("stormAppender".equals(appender.getName())) {
            System.out.println("storm appender has inited");
            return;
        }
    }
    System.out.println("init storm log to kafka topic " + StormEnvironment.getName(KafkaTopics.STORM_LOG));
    final Appender stormAppender = StormAppender.createAppender("stormAppender", StormEnvironment.getName(KafkaTopics.STORM_LOG), null, layout, true);
    stormAppender.start();
    config.addAppender(stormAppender);
    AppenderRef stormAppenderRef = AppenderRef.createAppenderRef("stormAppender", Level.INFO, null);
    final Appender appender = AsyncAppender.newBuilder().setIgnoreExceptions(true).setConfiguration(config)
            .setName("asyncStormAppender").setIncludeLocation(true).setBlocking(false).setAppenderRefs(new AppenderRef[] {stormAppenderRef}).build();
    appender.start();
    config.getRootLogger().addAppender(appender, Level.INFO, null);

      主要修改:

      a. 这里自定义的Appender用AsyncAppender包了一层,一定要设置setIncludeLocation(true),不然无法打印日志调用的位置

    五.  Storm不只是WordCount

    1. streamid的概念

      strorm资料比较多的是讲word count的例子,http://zqhxuyuan.github.io/2016/06/30/Hello-Storm/ 这篇文章讲了streamid的概念,让流可以分叉,可以合并

      一个bolt可以同时定义多种输入,一个bolt或者spout在emit数据的时候也可以到任意的spout或者bolt

      mergeBolt接收两种输入

    builder.setBolt("merge", merge, mergeThread)
                    .fieldsGrouping("recoverSpout", "merge-recover", new Fields("ideaId"))
                    .fieldsGrouping("log", new Fields("ideaId"));

      定义两种输出流,可以在运行时emit的时候指定streamid输出到哪个bolt

    @Override
        public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
            outputFieldsDeclarer.declare(new Fields("encrypt", "topic", "messageId"));
            outputFieldsDeclarer.declareStream("merge-recover", new Fields("param", "campaignId", "ideaId", "messageId", "topic"));
        }

    2. ack

      开启ack机制后需要注意如下几点:

      2.1 Spout需要自己存储未确认的消息,一般存储在一个LRUMapV2<String, Object> pending这样的定长队列中,防止爆内存

      2.2 Spout ack时删除pending数据

      2.3 Spout fail时一般选择重新投递,如果失败次数过多只记录日志,防止死消息在拓扑中无限循环

      2.4 messageId 最好由topology上游业务生成,更安全

      2.5 因为有重投存在,所以所有需要写数据的地方需要保持幂等性,用messageId + 操作做key,redis集群来存储,比如一个IncBolt -> SaveBolt,在SaveBolt失败,重投的时候IncBolt不需要在做具体的Inc操作

      ack机制不作为业务处理失败时的恢复机制:

      每个bolt在finally中进行ack,如果消息处理有异常放入到外部队列,手动进行重投,而不是依靠storm进行自动重投。让storm的ack机制保证的仅仅是当某个bolt挂掉时候的自动重投

    3. 慎用静态变量

      首先topology中的bolt和spout都是worker中的线程,可以使用jstack查看,如果一个boltA和boltB在同一个jvm中同时操作static变量可能会造成问题,所以最好不要使用单例,尤其是有状态的类,一定要在open的时候创建一个实例

      如果是资源类的,比如数据库连接,可以是各种bolt spout共享一个,将其设置为静态,但是注意初始化的时候需要synchronize

     

    4. bolt异常退出问题

      bolt的execute(Tuple input)方法,一定要加try catch exception finally,如果为execute的异常抛出到storm框架,会导致worker异常退出,整个拓扑stop,建议是异常自己catch并处理

    5. 长时间空消息导致Spout emptyEmit

      在测试环境发现Spout长时间的没有消息emit会出现一只调用emptyEmit,但是也不是一定出现,为了安全做了两点

      5.1 Spout从kafka消费的时候增加超时时间,当超时以后nextTuple立刻return 

      5.2 长时间没有emit消失,Spout emit一条空消息,bolt收到空消息只ack不处理

  • 相关阅读:
    ASP.NET操作文件大全
    Jquery1.7中文文档提供下载了
    修改server2005数据库的区分大小写设置
    SQL SERVER 设置自动备份和删除旧的数据库文件
    ASP.NET关闭下载窗口
    DB2通用分页存储过程
    ASP.NET生成压缩文件(rar打包)
    上传文件实体类
    【Demo 0104】注册/注销热键
    【Demo 0018】SEH结束处理程序
  • 原文地址:https://www.cnblogs.com/23lalala/p/8351770.html
Copyright © 2020-2023  润新知