Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 15907 articles
Browse latest View live

[译] 为何要对生产环境的 Node.js 使用反向代理?

$
0
0

原文: medium.com/intrinsic/w…

早在 2012 年,PHP 和 Ruby on Rails 仍是统治渲染 web 应用的服务器端技术。但是,一个大胆的竞争者在社区中搅起风暴。这项在当年的一篇博文中宣称通过了百万级并发连接测试的技术并非其他,正是 Node.js,其流行程度从那时至今也在稳定增长。

不像当时的大多数同类技术,Node.js 自带一个 web server。拥有这个 server 意味着开发者可以绕过诸如 php.ini的种种配置文件以及一个包含了若干 .htaccess文件的层级结构。内置的 web server 同样提供了其他便利,如处理上传文件的能力或易于实现 WebSockets 等。

由 Node.js 驱动的 web 应用每天都顺畅地处理数以亿计的请求。世界上大多数大公司都在某些方面被 Node.js 影响。说 Node.js 可用于生产环境肯定是个保守的说法。但是,对于构建 Node.js 仍有一个适用的建议:

不应直接把一个 Node.js 进程暴露到 web 上,而应该将其隐藏在一个反向代理背后。

在我们揭示原因之前,先看看那是什么吧。

反向代理是什么?

一个反向代理(reverse proxy)基本上就是一种用于接受请求并将它们转发到某处的另一个 HTTP server 上的特殊 web server,也会接受响应并转发回给原始的请求者。

反向代理通常不会精确地发送原始请求,典型的做法是会在某些方面对请求作出修改。例如,如果反向代理位于 www.example.org:80并转发请求到 ex.example.org:8080的话,大概就是它重写了原始的 Host header 以匹配目标地址。反向代理也可能通过其他方式修改请求,比如清理一个有缺陷的请求或是在协议之间作出转换。

一旦反向代理收到一个响应,可能也会对其进行某方面的转换。常用的途径同样是修改 Host header 以匹配原始请求。

请求的 body 也能被修改。一种通常的修改是在响应时执行 gzip 压缩。另一种常见改变是当基础服务只是 HTTP 时启用 HTTPS 支持。

反向代理也可以将到来的请求分发给多个后端实例。如果一个服务暴露于 api.example.org,那么一个反向代理可以转发请求到 api1.internal.example.orgapi2.internal.example.org等处。

有很多种不同的反向代理。两种更流行的分别是 NginxHAProxy。这两种工具都能够执行 gzip 压缩和增加 HTTPS 支持,它们也能很好地适用于其它领域。Nginx 更流行一些,并且也有诸如从文件系统运行静态文件服务器等一些其它有益的能力,所以我们将在本文中使用它作为例子。

现在我们知道 何为反向代理了,下面来看看 为何我们要将其用于 Node.js。

为何应该使用一个反向代理?

SSL 终端

SSL 终端是使用反向代理的最主要原因之一。将一个应用的协议从 http变为 https可不止添加一个 s字母那么简单。Node.js 本身 能够https执行必要的加密和解密,也能被配置为读取必要的证书文件。

但是,配置一个用于和我们的应用通信的协议,并管理一个永不过期的 SSL 凭证,并非真的是我们的应用需要关注的事情。检查一个代码库中的证书不只是麻烦,同时也是一种安全风险。在应用启动时从特定位置获取证书也有其风险。

有鉴于此,最好在应用之外执行 SSL 终端,通常就由一个反向代理来承担。受惠于像 certbot这样的技术,通过 Nginx 维护证书也像设置一个定时任务一样简单。这样的一个任务可以自动安装新证书并动态重配置 Nginx 进程。与重启每个 Node.js 应用实例相比,这是一个破坏性小得多的过程。

同时,通过允许一个反向代理来执行 SSL 终端,这也意味着 只有被反向代理的作者编写的代码可以访问你的私有 SSL 证书。反之,如果由你的 Node.js 应用处理 SSL,那么你的应用用到的每一个单独的第三方模块 -- 即便是潜在的恶意模块,都能访问你的私有 SSL 证书了。

gzip 压缩

gzip 是另一个你应该由应用让渡到反向代理的特性。gzip 压缩策略最好在管理级别设置,而不是不得不为每个应用指定和配置。

最好使用某些逻辑来决定对什么采用 gzip。例如,很小的文件,或许比 1kb 还小的,或许就不值得压缩,因为其 gzip 压缩版本没准会变得更大,亦或客户端对其解压的 CPU 开销也更不划算。同时,当处理二进制数据时,取决于其格式可能也无法从压缩中获益。有时 gzip 也无法被简单地启用或禁用,为了兼容压缩算法需要检查接收到的 Accept-Encoding header。

集群化

JavaScript 是一种单线程语言,相应的,Node.js 天然也是一种单线程的服务器平台(尽管 Node.js v10 中开始出现的实验性 worker 线程支持致力于改变这一点)。这意味着要从一个 Node.js 应用中获取尽可能更大的吞吐量需要运行和 CPU 核数差不多相同的实例数量。

Node.js 自带的 cluster模块可以实现集群化。收到的 HTTP 请求将会被交给一个 master 进程而后再被分发给 cluster workers。

但是,动态伸缩 cluster workers 会有点费劲。通常也会通过运行一个额外的 Node.js 进程作为分发主进程来增加吞吐量。但是,跨机器伸缩进程对于 cluster来说还是有点强人所难了。

基于这些原因,有时使用一个反向代理来分发正在运行的 Node.js 进程会更好。反向代理能被动态配置以指向新出现的应用进程。说实在的,一个应用就应该只关注其自身的工作,而不是管理多个拷贝并分发请求。

企业路由

当着手于大型 web 应用,特别是被有多个团队的企业创建的应用时,使用一个反向代理来决定如何转发请求是非常有用的。例如,指向 example.org/search/*的请求可被路由到内部搜索应用,而其它指向 example.org/profile/*的请求可以被分发到内部资料应用上。

这样的加工处理提供了其它强大的特性,如粘滞会话、蓝/绿部署、A/B测试等等。我个人就曾在这样一个由应用执行这些逻辑的代码库中工作,这种实现方式让应用极难维护。

性能收益

Node.js 是高可塑性的。它可以从文件系统架设静态资源服务、对 HTTP 响应执行 gzip 压缩、内建支持 HTTPS,另有很多其它特性。它甚至有能力通过 cluster模块,运行一个应用的多个实例并分发其自身的请求。

然而,基本上让一个反向代理来处理这些操作,而不是靠 Node.js 应用去做,才是符合我们利益的。除了以上列出的原因之外,另一个想要在 Node.js 之外做这些操作的原因是由于效率。

SSL 加密和 gzip 压缩是两个高计算密集型的操作。专用型反向代理工具,如 Nginx 和 HAProxy,对这些操作术业有专攻,执行速度要快于 Node.js。用 Nginx 这样的 web server 从磁盘上读取静态内容也比 Node.js 来得快。有时甚至比起用额外的 Node.js 进程来执行集群化,用 Nginx 反向代理实现的效率都更高,内存和 CPU 的占用都更少。

但是,耳听为虚。让我们运行一些基准测试!

以下负载测试是使用 siege(译注:一个 http/https 回归测试和基准测试工具)执行的。我们用并发值 10 运行命令(模拟用户的请求),并且该命令会迭代运行 2 万次(总体会有 20 万次请求)。

为检验内存使用量我们在基准测试期间运行命令 pmap | grep total若干次并取 平均值作为结果(译注:Linux 中的 pmap 命令用于查看进程用了多少内存)。当使用单个 worker 线程运行 Nginx 时,最终会运行两个实例,一个是主线程,另一个是 worker 线程;然后将两个值相加。当运行一个包含 2 个节点的 Node.js 集群时,将有 3 个进程,一个是主进程,另两个是 worker 进程。下表中的 approx memory列是给定测试中每个 Nginx 和 Node.js 进程的总和。

基准测试的结果

node-cluster基准测试中我们使用了 2 个 worker,这意味着共有 3 个 Node.js 进程在运行:1 个 master 和 2 个 worker。在 nginx-cluster-node基准测试中我们有 2 个 Node.js 进程在运行。每个 Nginx 测试都有一个单独的 Nginx 主进程和一个单独的 Nginx worker 进程。基准测试包括从磁盘读取一个文件,且无论是 Nginx 还是 Node.js 都被配置为将文件缓存在内存中。

使用 Nginx 为 Node.js 执行 SSL 终端带来了约 16% (746rps 到 865rps) 的吞吐量增长。使用 Nginx 执行 gzip 压缩则让吞吐量增加了约 50% (5,047rps 到 7,590rps)。使用 Nginx 管理一个进程集群造成了约 1% (8,006rps 到 7,908rps) 的性能损失,大概是归因于在回环网络设备间传递额外请求的开销吧。

一个基本的 Node.js 单进程单内存使用量是约 600MB,而 Nginx 进程的约 50MB。这些使用量会根据使用了那些特性而小幅波动,例如,Node.js 使用了额外的约 13MB来执行 SSL 终端,以及 Nginx 使用了额外的 4MB(译注:652 - 601 - 46.1)来作为 Node.js 的反向代理并架设从文件系统读取静态内容的服务。可以注意到一个有趣的事情是 Nginx 在其生命周期中保持了稳定的内存吞吐量;然而 Node.js 的内存吞吐量由于 JavaScript 天然的垃圾回收机制而时常起伏。

以下是执行此次基准测试的软件版本:

  • Nginx: 1.14.2
  • Node.js: 10.15.3
  • Siege: 3.0.8

测试执行在一台有 16GB内存的机器上,有一块 i7-7500U CPU 4x2.70GHz,Linux 内核为 4.19.10。项目地址位于: github.com/IntrinsicLa…

简化应用代码

基准测试全面又出彩,但从我的观点来说,将部分工作从 Node.js 应用让渡给反向代理带来的最大收益是代码的简化。我们得以减少了大量可能造成潜在 bug 的必须应用代码并将其换成了声明式的配置。开发者中的一个普遍观点是他们对于让 Nginx 等外部团队编写这部分代码,而不是由他们自己编写更有信心。

不同于要安装和管理 gzip 压缩中间件并在多个 Node.js 项目中保持其最新,我们可以在一处统一配置它。和加载 SSL 证书后再重启应用进程不同,我们可以使用已有的证书管理工具。与其在应用中添加条件语句检查进程是 master 还是 worker,不如将其交给其它工具判断。

反向代理允许我们的应用聚焦于业务逻辑并忽略协议和进程管理。


尽管 Node.js 拥有运行在生产环境的完美能力,但将反向代理和生产环境的 HTTP Node.js 应用结合使用带来了种种收益。像 SSL 和 gzip 这样的操作变得更快。管理 SSL 证书也会更简单。大量所需应用代码也被减少了。



--End--

查看更多前端好文
请搜索 fewelife 关注公众号

转载请注明出处


定时任务莫名停止,Spring 定时任务存在 Bug?? - 楼下小黑哥 - 博客园

$
0
0

Hello~各位读者新年好!这里楼下小黑哥给大家拜个年,祝大家蒸蒸日上烫烫烫,年年有余屯屯屯。

那年那 Bug

春节放假,小黑哥坐上高铁回家,突然想到一次生产问题。那是小黑哥参加工作第一年,那一年国庆假期,小黑哥提前一天请假回家办个护照。那时候刚开始负责一个生产系统,所以工作日请假,还是有点担心,就怕 问题看小黑哥不在,悄然上门。

哎,真实越怕什么,就来什么。

高铁开到一半的时候,同事反馈系统不能获取最新的流水信息(流水信息通过 Spring定时任务定时拉取)。小黑哥心里一惊,立刻拔出电脑,连上 VPN,准备登上生产机器,查看系统情况。可是,高铁上网络大家也懂,很不稳定,连了好久连不上 VPN,只好远程指挥同事看一下系统日志。通过同事反馈的日志,发现拉取流水定时任务没有执行,进一步查看,小黑哥发现整个系统其他的定时任务也都停止了。。。

这真是一个奇怪的的问题,这好端端的定时任务怎么会突然停止?

暂时想不到解决办法,只好指挥同事先重启应用。重启之后,暂时解决问题,定时任务重新开始执行,也获取到最新的付款流水信息。

问题排查

到家之后,小黑哥立刻登上生产机器,查看系统日志,发现重启之前某一定时任务运行到一半,并且在这之后其他定时任务就没有再被执行。

通过系统日志,定位到了有问题的代码。

这里采用 重试补偿策略,防止查询流水信息因为网络等问题发生偶发的失败。这个策略面对偶发的失败没什么问题,但是如果查询银行流水服务一直失败,这段代码就会陷入死循环。恰巧那段时间网络出现一些问题,导致这里查询一直处于失败。

增加最大重试次数,修复该 Bug

修复之后,立刻将最新版本代码部署到生产系统,暂时解决了这个问题。

知识点:面对一些失败,可以采用重试补偿策略,重新执行,最大可能保证执行成功,但是这里切记设置合适的的 重大的次数

深入排查

虽然问题解决了,但是小黑哥心里还是存在一个疑惑,为何一个定时任务发生了阻塞,就会影响执行其他定时任务。小黑哥最初的理解是不同的定时任务应该互相隔离,互不影响才对,真难到是 Spring定时任务的 Bug吗?

想到这里,小黑哥决定写一个 Demo,复现问题,然后深入源码排查。

启动程序,日志输出如下:

image-20200124160151622

从日志可以看到, fixDelayMethod方法执行之后进入休眠,直到休眠结束, cronMethod定时任务才有机会被执行。另外从上面可以看到,上述两个定时任务都由 pool-1-thread-1线程执行。从这点可以看出 Spring定时任务将会交给线程池执行。

知识点: 线程池中线程默认命名策略为 pool-%poolNumber-thread-%num。

如果线程池只有一个工作线程,该线程一旦被长时间阻塞,堆积的其他任务就没有机会被执行。

那么是不是这个问题导致的 Sping定时任务停止执行?我们继续往下排查。

图上日志绿色部分, ScheduledAnnotationBeanPostProcessor输出一个重要信息:

No TaskScheduler/ScheduledExecutorService bean found for scheduled processing

查看 Spring文档Spring内部将会通过调用 TaskScheduler执行定时任务,而另一个 ScheduledExecutorServiceJDK提供执行定时任务的执行器。记住这两者

image-20200125140457573

通过这段日志,使用 IDEA 的强大的 关键字搜索功能,定位到 ScheduledAnnotationBeanPostProcessor#finishRegistration方法。

这个方法比较长,大家重点关注图中标示的几处。

Spring启动之后将会扫描所有 Bean中带有 @Scheduled注解的方法,然后封装成 Task子类放置到 ScheduledTaskRegistrar

这段代码位于 ScheduledAnnotationBeanPostProcessor#processScheduled,感兴趣的可以翻阅查看

如果此时 ScheduledTaskRegistrar不存在定时任务或者 ScheduledTaskRegistrar中的 TaskScheduler不存在, finishRegistration将会多次调用 ScheduledAnnotationBeanPostProcessor#resolveSchedulerBean方法用以查找 TaskScheduler/ScheduledExecutorService

接下去将会把获取到 Bean通过 setScheduler注入到 ScheduledTaskRegistrar中。

如果获取的为 ScheduledExecutorService类型,将会将其封装到 taskScheduler中。

最后还没找到,将会输出最刚开始见到的日志。然后 Spirng将会在 ScheduledTaskRegistrar#afterPropertiesSet创建一个单线程的定时任务执行器 ScheduledExecutorService,注入到 ConcurrentTaskScheduler中,然后通过 taskScheduler执行定时任务。

image-20200125144040781

交给 TaskScheduler的定时任务最后实际上还是通过 ScheduledExecutorService执行。

这里可以得出一个 结论

Spring定时任务实际上通过 JDK提供的 ScheduledExecutorService执行。默认情况下,Spring 将会生成一个单线程 ScheduledExecutorService执行定时任务。所以一旦某一个定时任务长时间阻塞这个执行线程,其他定时任务都将被影响,没有机会被执行线程执行。

Spring 这种默认配置,在需要执行多个定时任务的情况,可能会是一个坑。我们可以通过改变配置,使 Spring 采用多线程执行定时任务。

自定义配置

Spring 可以通过多种方式改变默认配置。

xml 配置

通过 xml配置 TaskScheduler线程数。

<task:annotation-driven  scheduler="myScheduler"/><task:scheduler id="myScheduler" pool-size="10"/>

通过上面的配置,Spring 将会使用 TaskScheduler子类 ThreadPoolTaskScheduler,内部线程数为 pool-size数量,这个线程数将会直接设置 ScheduledExecutorService线程数量。

注解配置

在上面问题排查中,我们知道 Spring将会查找 TaskScheduler/ScheduledExecutorService,若存在将会使用。所以这里我们可以生成这些类的 Bean

以上方式二选一即可

SpringBoot 配置

上面两种配置适用于普通 Spring,比较繁琐。相比而言 SpringBoot配置将会非常简单,只需要在启动配置文件加入如下配置即可。

spring.task.scheduling.pool.size=10
spring.task.scheduling.thread-name-prefix=task-test

技术总结

下面开始技术总结:

  1. Spring定时任务执行原理实际使用的是 JDK自带的 ScheduledExecutorService
  2. Spring默认配置下,将会使用具有单线程的 ScheduledExecutorService
  3. 单线程执行定时任务,如果某一个定时任务执行时间较长,将会影响其他定时任务执行
  4. 如果存在多个定时任务,为了保证定时任务执行时间的准确性,可以修改默认配置,使其使用多线程执行定时任务
  5. 面对偶发的失败,我们可以采用重试补偿策略,不过这里切记设置合适的最大重试次数

随便聊聊

对于常用的开源框架,我们不仅要掌握怎么用,还要熟悉相关的配置,最后还应该去了解其内部的使用的原理。这样出了问题,我们也能很快定位问题,找到问题的实际原因。

帮助文档

Spring scheduling-task-scheduler

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客: studyidea.cn


Cppentry程序开发

$
0
0

最近修改公司线上kafka集群配置然后直接kill掉进程来重启集群发现所有生产者都无法写入数据导致丢了数据,栽了一个大坑,接下来的工作肯定是补坑找原因,就分享一下。

系统环境说明:kafka版本为0.8.1.1,kafka集群配置为10.12.0.23:2181,10.12.0.24:2181,10.12.0.25:2181/kafka,因此在zookeeper中的根路径为:/kafka
1.先决条件,kafka启动必须加上JMX_PORT=9999(自己选一个空闲端口)
在zookeeper的客户端使用get /kafka/brokers/ids/[0,1,2](一个数字代表你的brokerid),如果返回信息中的jmx_port 值为-1就说明启动时没有指定JMX_PORT,这样不需要进行下面步骤,请直接重启集群加上配置,并且server.properties需要设置 auto.create.topics.enable=false
2.观察状态
使用命令./bin/kafka-topics.sh --zookeeper 10.12.0.23:2181,10.12.0.24:2181,10.12.0.25:2181/kafka --describe --topic checkall
观察当前topic的partition分布状况,以单个topic作为说明,此时状态如下:
Topic:checkallPartitionCount:2ReplicationFactor:2Configs:
Topic: checkallPartition: 0Leader: 1Replicas: 1,0Isr: 0,1
Topic: checkallPartition: 1Leader: 0 Replicas: 0,1Isr: 0,1
3.让某台机器下线
./bin/kafka-run-class.sh kafka.admin.ShutdownBroker --zookeeper 10.12.0.23:2181,10.12.0.24:2181,10.12.0.25:2181/kafka --broker 1 --num.retries 3 --retry.interval.ms 600
如出错: java.io.IOException: Failed to retrieve RMIServer stub: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: XXXXXXXX; nested exception is 请回到步骤1
如成功:再次观察状态可以看到:
Topic:checkallPartitionCount:2ReplicationFactor:2Configs:
Topic: checkallPartition: 0Leader: 0Replicas: 1,0Isr: 0,1
Topic: checkallPartition: 1Leader: 0Replicas: 0,1Isr: 0,1
此时leader已经全部切到brokerid为 0的机器上了
4.JPS找到kafka进程号并kill掉
5.leader重新均衡
./bin/kafka-preferred-replica-election.sh --zookeeper 10.12.0.23:2181,10.12.0.24:2181,10.12.0.25:2181/kafka
成功后重新观察状态会发现恢复到原来正常情况了。

转载请注明来自:http://blog.csdn.net/clarencezi/article/details/42271037

基于OGG的Oracle与Hadoop集群准实时同步介绍 - 偶素浅小浅 - 博客园

利用ogg实现oracle到kafka的增量数据实时同步 | 伦少的博客

$
0
0

前言

ogg即Oracle GoldenGate是Oracle的同步工具,本文讲如何配置ogg以实现Oracle数据库增量数据实时同步到kafka中,其中同步消息格式为json。
下面是我的源端和目标端的一些配置信息:

-版本OGG版本ip别名
源端OracleRelease 11.2.0.1.0Oracle GoldenGate 11.2.1.0.3 for Oracle on Linux x86-64192.168.44.128master
目标端kafka_2.11-1.1.0Oracle GoldenGate for Big Data 12.3.1.1.1 on Linux x86-64192.168.44.129slave1

1、下载

可在 这里旧版本查询下载
注意:源端和目标端的文件不一样,目标端需要下载Oracle GoldenGate for Big Data,源端需要下载Oracle GoldenGate for Oracle具体下载方法见最后的附录截图。

2、源端(Oracle)配置

注意:源端是安装了oracle的机器,oracle环境变量之前都配置好了

2.1 解压

先建立ogg目录

1             
2
mkdir -p /opt/ogg             
unzip V34339-01.zip

解压后得到一个tar包,再解压这个tar

1             
2
tar xf fbo_ggs_Linux_x64_ora11g_64bit.tar -C /opt/ogg             
chown -R oracle:oinstall /opt/ogg (使oracle用户有ogg的权限,后面有些需要在oracle用户下执行才能成功)

2.2 配置ogg环境变量

为了简单方便起见,我在/etc/profile里配置的,建议在生产中配置oracle的环境变量文件/home/oracle/.bash_profile里配置,为了怕出问题,我把OGG_HOME等环境变量在/etc/profile配置了一份,不知道这是否是必须的。

1             
vim /etc/profile             

1            
2
3
export OGG_HOME=/opt/ogg            
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:/usr/lib
export PATH=$OGG_HOME:$PATH

使之生效

1             
source/etc/profile             

测试一下ogg命令

1             
ggsci             

如果命令成功即可进行下一步,不成功请检查前面的步骤。

2.3 oracle打开归档模式

1            
2
su - oracle            
sqlplus / as sysdba

执行下面的命令查看当前是否为归档模式

1             
archive log list             

1            
2
3
4
5
6
SQL> archive log list            
Database log mode No Archive Mode
Automatic archival Disabled
Archive destination USE_DB_RECOVERY_FILE_DEST
Oldest online log sequence 12
Current log sequence 14

若为Disabled,手动打开即可

1             
2
3
4
5
6
conn / as sysdba (以DBA身份连接数据库)             
shutdown immediate (立即关闭数据库)
startup mount (启动实例并加载数据库,但不打开)
alterdatabasearchivelog; (更改数据库为归档模式)
alterdatabaseopen; (打开数据库)
altersystemarchivelogstart; (启用自动归档)

再执行一下

1             
archive log list             

1            
2
3
4
5
6
Database log mode	       Archive Mode            
Automatic archival Enabled
Archive destination USE_DB_RECOVERY_FILE_DEST
Oldest online log sequence 12
Next log sequence to archive 14
Current log sequence 14

可以看到为Enabled,则成功打开归档模式。

2.4 Oracle打开日志相关

OGG基于辅助日志等进行实时传输,故需要打开相关日志确保可获取事务内容,通过下面的命令查看该状态

1             
selectforce_logging, supplemental_log_data_minfromv$database;             

1            
2
3
FORCE_ SUPPLEMENTAL_LOG            
------ ----------------
NO NO

若为NO,则需要通过命令修改

1             
2
alterdatabaseforcelogging;             
alterdatabaseaddsupplementallogdata;

再查看一下为YES即可

1             
2
3
4
5
SQL> select force_logging, supplemental_log_data_min from v$database;             

FORCE_ SUPPLEMENTAL_LOG
------ ----------------
YES YES

2.5 oracle创建复制用户

首先root用户建立相关文件夹,并赋予权限

1             
2
mkdir -p /u01/app/oracle/oggdata/orcl             
chown -R oracle:oinstall /u01/app/oracle/oggdata/orcl

然后执行下面sql

1             
2
3
4
5
6
7
8
9
10
11
SQL> create tablespace oggtbs datafile '/u01/app/oracle/oggdata/orcl/oggtbs01.dbf' size 1000M autoextend on;             

Tablespace created.

SQL> create user ogg identified by ogg default tablespace oggtbs;

User created.

SQL> grant dba to ogg;

Grantsucceeded.

2.6 OGG初始化

1            
2
ggsci            
create subdirs
1            
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ggsci            

Oracle GoldenGate Command InterpreterforOracle
Version 11.2.1.0.3 14400833 OGGCORE_11.2.1.0.3_PLATFORMS_120823.1258_FBO
Linux, x64, 64bit (optimized), Oracle 11g on Aug 23 2012 20:20:21

Copyright (C) 1995, 2012, Oracle and/or its affiliates. All rights reserved.



GGSCI (ambari.master.com) 1> create subdirs

Creating subdirectories under current directory /root

Parameter files /root/dirprm: created
Report files /root/dirrpt: created
Checkpoint files /root/dirchk: created
Process status files /root/dirpcs: created
SQL script files /root/dirsql: created
Database definitions files /root/dirdef: created
Extract data files /root/dirdat: created
Temporary files /root/dirtmp: created
Stdout files /root/dirout: created


GGSCI (ambari.master.com) 2>

2.7 Oracle创建测试表

创建一个用户,在该用户下新建测试表,用户名、密码、表名均为 test_ogg。

1             
2
3
4
createusertest_oggidentifiedbytest_oggdefaulttablespaceusers;             
grantdbatotest_ogg;
conn test_ogg/test_ogg;
createtabletest_ogg(idint,namevarchar(20),primarykey(id));

3 目标端(kafka)配置

1            
2
3
mkdir -p /opt/ogg            
unzip 123111_ggs_Adapters_Linux_x64.zip
tar xf ggs_Adapters_Linux_x64.tar -C /opt/ogg/

3.2 环境变量

1            
vim /etc/profile            
1            
2
3
export OGG_HOME=/opt/ogg            
export LD_LIBRARY_PATH=$JAVA_HOME/jre/lib/amd64:$JAVA_HOME/jre/lib/amd64/server:$JAVA_HOME/jre/lib/amd64/libjsig.so:$JAVA_HOME/jre/lib/amd64/server/libjvm.so:$OGG_HOME/lib
export PATH=$OGG_HOME:$PATH
1            
source/etc/profile            

同样测试一下ogg命令

1             
ggsci             

3.3 初始化目录

1            
create subdirs            

4、OGG源端配置

4.1 配置OGG的全局变量

先切换到oracle用户下

1             
2
3
su oracle             
cd/opt/ogg
ggsci

1            
2
3
4
GGSCI (ambari.master.com) 1> dblogin userid ogg password ogg            
Successfully logged into database.

GGSCI (ambari.master.com) 2> edit param ./globals

然后和用vim编辑一样添加

1             
oggschema ogg             

4.2 配置管理器mgr

1            
2
3
4
5
GGSCI (ambari.master.com) 3> edit param mgr            
PORT 7809
DYNAMICPORTLIST 7810-7909
AUTORESTART EXTRACT *,RETRIES 5,WAITMINUTES 3
PURGEOLDEXTRACTS ./dirdat/*,usecheckpoints, minkeepdays 3

说明:PORT即mgr的默认监听端口;DYNAMICPORTLIST动态端口列表,当指定的mgr端口不可用时,会在这个端口列表中选择一个,最大指定范围为256个;AUTORESTART重启参数设置表示重启所有EXTRACT进程,最多5次,每次间隔3分钟;PURGEOLDEXTRACTS即TRAIL文件的定期清理

4.3 添加复制表

1            
2
3
4
5
6
7
8
9
GGSCI (ambari.master.com) 4> add trandata test_ogg.test_ogg            

Logging of supplemental redo data enabledfortable TEST_OGG.TEST_OGG.

GGSCI (ambari.master.com) 5> info trandata test_ogg.test_ogg

Logging of supplemental redologdata is enabledfortable TEST_OGG.TEST_OGG.

Columns supplementally loggedfortable TEST_OGG.TEST_OGG: ID

4.4 配置extract进程

1            
2
3
4
5
6
7
8
GGSCI (ambari.master.com) 6> edit param extkafka            
extract extkafka
dynamicresolution
SETENV (ORACLE_SID ="orcl")
SETENV (NLS_LANG ="american_america.AL32UTF8")
userid ogg,password ogg
exttrail /opt/ogg/dirdat/to
table test_ogg.test_ogg;

说明:第一行指定extract进程名称;dynamicresolution动态解析;SETENV设置环境变量,这里分别设置了Oracle数据库以及字符集;userid ggs,password ggs即OGG连接Oracle数据库的帐号密码,这里使用2.5中特意创建的复制帐号;exttrail定义trail文件的保存位置以及文件名,注意这里文件名只能是2个字母,其余部分OGG会补齐;table即复制表的表名,支持*通配,必须以;结尾

添加extract进程:

1            
2
GGSCI (ambari.master.com) 16> add extract extkafka,tranlog,begin now            
EXTRACT added.

(注:若报错

1             
ERROR: Could not create checkpoint file /opt/ogg/dirchk/EXTKAFKA.cpe (error 2, No such file or directory).             

执行下面的命令再重新添加即可。

1             
create subdirs             

)

添加trail文件的定义与extract进程绑定:

1            
2
GGSCI (ambari.master.com) 17> add exttrail /opt/ogg/dirdat/to,extract extkafka            
EXTTRAIL added.

4.5 配置pump进程

pump进程本质上来说也是一个extract,只不过他的作用仅仅是把trail文件传递到目标端,配置过程和extract进程类似,只是逻辑上称之为pump进程

1             
2
3
4
5
6
7
8
GGSCI (ambari.master.com) 18> edit param pukafka             
extract pukafka
passthru
dynamicresolution
userid ogg,password ogg
rmthost 192.168.44.129 mgrport 7809
rmttrail /opt/ogg/dirdat/to
table test_ogg.test_ogg;

说明:第一行指定extract进程名称;passthru即禁止OGG与Oracle交互,我们这里使用pump逻辑传输,故禁止即可;dynamicresolution动态解析;userid ogg,password ogg即OGG连接Oracle数据库的帐号密码rmthost和mgrhost即目标端(kafka)OGG的mgr服务的地址以及监听端口;rmttrail即目标端trail文件存储位置以及名称。

分别将本地trail文件和目标端的trail文件绑定到extract进程:

1             
2
3
4
GGSCI (ambari.master.com) 1> add extract pukafka,exttrailsource /opt/ogg/dirdat/to             
EXTRACT added.
GGSCI (ambari.master.com) 2> add rmttrail /opt/ogg/dirdat/to,extract pukafka
RMTTRAIL added.

4.6 配置define文件

Oracle与MySQL,Hadoop集群(HDFS,Hive,kafka等)等之间数据传输可以定义为异构数据类型的传输,故需要定义表之间的关系映射,在OGG命令行执行:

1             
2
3
4
GGSCI (ambari.master.com) 3> edit param test_ogg             
defsfile /opt/ogg/dirdef/test_ogg.test_ogg
userid ogg,password ogg
table test_ogg.test_ogg;

在OGG主目录下执行(oracle用户):

1             
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
./defgen paramfile dirprm/test_ogg.prm             

***********************************************************************
Oracle GoldenGate Table Definition GeneratorforOracle
Version 11.2.1.0.3 14400833 OGGCORE_11.2.1.0.3_PLATFORMS_120823.1258
Linux, x64, 64bit (optimized), Oracle 11g on Aug 23 2012 16:58:29

Copyright (C) 1995, 2012, Oracle and/or its affiliates. All rights reserved.


Starting at 2018-05-23 05:03:04
***********************************************************************

Operating System Version:
Linux
Version#1 SMP Wed Apr 12 15:04:24 UTC 2017, Release 3.10.0-514.16.1.el7.x86_64
Node: ambari.master.com
Machine: x86_64
softlimithardlimit
Address Space Size : unlimited unlimited
Heap Size : unlimited unlimited
File Size : unlimited unlimited
CPU Time : unlimited unlimited

Process id: 13126

***********************************************************************
** Running with the following parameters **
***********************************************************************
defsfile /opt/ogg/dirdef/test_ogg.test_ogg
userid ogg,password ***
table test_ogg.test_ogg;
Retrieving definitionforTEST_OGG.TEST_OGG



Definitions generatedfor1 tablein/opt/ogg/dirdef/test_ogg.test_ogg

将生成的/opt/ogg/dirdef/test_ogg.test_ogg发送的目标端ogg目录下的dirdef里:

1             
scp -r /opt/ogg/dirdef/test_ogg.test_ogg root@slave1:/opt/ogg/dirdef/             

5、OGG目标端配置

5.1 开启kafka服务

1            
2
3
cd/opt/kafka_2.11-1.1.0/            
bin/zookeeper-server-start.sh config/zookeeper.properties
bin/kafka-server-start.sh config/server.properties

5.2 配置管理器mgr

1            
2
3
4
5
GGSCI (ambari.slave1.com) 1>  edit param mgr            
PORT 7809
DYNAMICPORTLIST 7810-7909
AUTORESTART EXTRACT *,RETRIES 5,WAITMINUTES 3
PURGEOLDEXTRACTS ./dirdat/*,usecheckpoints, minkeepdays 3

5.3 配置checkpoint

checkpoint即复制可追溯的一个偏移量记录,在全局配置里添加checkpoint表即可。

1             
2
edit  param  ./GLOBALS             
CHECKPOINTTABLE test_ogg.checkpoint

5.4 配置replicate进程

1            
2
3
4
5
6
7
GGSCI (ambari.slave1.com) 4> edit param rekafka            
REPLICAT rekafka
sourcedefs /opt/ogg/dirdef/test_ogg.test_ogg
TARGETDB LIBFILE libggjava.so SET property=dirprm/kafka.props
REPORTCOUNT EVERY 1 MINUTES, RATE
GROUPTRANSOPS 10000
MAP test_ogg.test_ogg, TARGET test_ogg.test_ogg;

说明:REPLICATE rekafka定义rep进程名称;sourcedefs即在4.6中在源服务器上做的表映射文件;TARGETDB LIBFILE即定义kafka一些适配性的库文件以及配置文件,配置文件位于OGG主目录下的dirprm/kafka.props;REPORTCOUNT即复制任务的报告生成频率;GROUPTRANSOPS为以事务传输时,事务合并的单位,减少IO操作;MAP即源端与目标端的映射关系

5.5 配置kafka.props

1            
2
cd/opt/ogg/dirprm/            
vim kafka.props
1            
2
3
4
5
6
7
gg.handlerlist=kafkahandler //handler类型            
gg.handler.kafkahandler.type=kafka
gg.handler.kafkahandler.KafkaProducerConfigFile=custom_kafka_producer.properties //kafka相关配置
gg.handler.kafkahandler.topicMappingTemplate=test_ogg //kafka的topic名称,无需手动创建
gg.handler.kafkahandler.format=json //传输文件的格式,支持json,xml等
gg.handler.kafkahandler.mode=op //OGGforBig Data中传输模式,即op为一次SQL传输一次,tx为一次事务传输一次
gg.classpath=dirprm/:/opt/kafka_2.11-1.1.0/libs/*:/opt/ogg/:/opt/ogg/lib/*
1            
vim custom_kafka_producer.properties            
1            
2
3
4
5
6
7
8
bootstrap.servers=192.168.44.129:9092//kafkabroker的地址            
acks=1
compression.type=gzip //压缩类型
reconnect.backoff.ms=1000//重连延时
value.serializer=org.apache.kafka.common.serialization.ByteArraySerializer
key.serializer=org.apache.kafka.common.serialization.ByteArraySerializer
batch.size=102400
linger.ms=10000

其中需要将后面的注释去掉,ogg不识别注释,如果不去掉会报错

5.6 添加trail文件到replicate进程

1            
2
GGSCI (ambari.slave1.com) 2> add replicat rekafka exttrail /opt/ogg/dirdat/to,checkpointtable test_ogg.checkpoint            
REPLICAT added.

6、测试

6.1 启动所有进程

在源端和目标端的OGG命令行下使用start [进程名]的形式启动所有进程。
启动顺序按照源mgr——目标mgr——源extract——源pump——目标replicate来完成。
全部需要在ogg目录下执行ggsci目录进入ogg命令行。
源端依次是

1             
2
3
start mgr             
start extkafka
start pukafka

目标端

1             
2
start mgr             
start rekafka

可以通过info all 或者info [进程名] 查看状态,所有的进程都为RUNNING才算成功
源端

1             
2
3
4
5
6
7
GGSCI (ambari.master.com) 5> info all             

Program Status Group Lag at Chkpt Time Since Chkpt

MANAGER RUNNING
EXTRACT RUNNING EXTKAFKA 04:50:21 00:00:03
EXTRACT RUNNING PUKAFKA 00:00:00 00:00:03

目标端

1             
2
3
4
5
6
GGSCI (ambari.slave1.com) 3> info all             

Program Status Group Lag at Chkpt Time Since Chkpt

MANAGER RUNNING
REPLICAT RUNNING REKAFKA 00:00:00 00:00:01

6.2 异常解决

如果有不是RUNNING可通过查看日志的方法检查解决问题,具体通过下面两种方法

1             
vim ggser.log             

或者ogg命令行,以rekafka进程为例

1            
GGSCI (ambari.slave1.com) 2> view report rekafka            

列举其中我遇到的一个问题:
异常信息

1             
2
3
SEVERE: Unable tosetproperty on handler'kafkahandler'(oracle.goldengate.handler.kafka.KafkaHandler). Failed tosetproperty: TopicName:="test_ogg"(class: oracle.goldengate.handler.kafka.KafkaHandler).             
oracle.goldengate.util.ConfigException: Failed tosetproperty: TopicName:="test_ogg"(class: oracle.goldengate.handler.kafka.KafkaHandler).
at ......

具体原因是网上的教程是旧版的,设置topicName的属性为:

1             
gg.handler.kafkahandler.topicName=test_ogg             

新版的这样设置

1             
gg.handler.kafkahandler.topicMappingTemplate=test_ogg             

大家可根据自己的版本进行设置,附上stackoverflow原答案

1             
2
3
4
5
6
7
8
I tried to move data from Oracle Database to Kafka using Golden gate adapter Version 12.3.0.1.0             

In new version there is no topicname

The following resolves the topic name using the short table name
gg.handler.kafkahandler.topicMappingTemplate=test

In previous version we have gg.handler.kafkahandler.topicName=test

6.3 测试同步更新效果

现在源端执行sql语句

1             
2
3
4
5
6
7
conn test_ogg/test_ogg             
insert into test_ogg values(1,'test');
commit;
update test_oggsetname='zhangsan'whereid=1;
commit;
delete test_oggwhereid=1;
commit;

查看源端trail文件状态

1             
2
ls -l /opt/ogg/dirdat/to*             
-rw-rw-rw- 1 oracle oinstall 1464 May 23 10:31 /opt/ogg/dirdat/to000000

查看目标端trail文件状态

1             
2
ls -l /opt/ogg/dirdat/to*             
-rw-r----- 1 root root 1504 May 23 10:31 /opt/ogg/dirdat/to000000

查看kafka是否自动建立对应的主题

1             
bin/kafka-topics.sh --list --zookeeper localhost:2181             

在列表中显示有test_ogg则表示没问题
通过消费者看是否有同步消息

1             
2
3
4
bin/kafka-console-consumer.sh --bootstrap-server 192.168.44.129:9092 --topic test_ogg --from-beginning             
{"table":"TEST_OGG.TEST_OGG","op_type":"I","op_ts":"2018-05-23 10:31:28.000078","current_ts":"2018-05-23T10:36:48.525000","pos":"00000000000000001093","after":{"ID":1,"NAME":"test"}}
{"table":"TEST_OGG.TEST_OGG","op_type":"U","op_ts":"2018-05-23 10:31:36.000073","current_ts":"2018-05-23T10:36:48.874000","pos":"00000000000000001233","before":{},"after":{"ID":1,"NAME":"zhangsan"}}
{"table":"TEST_OGG.TEST_OGG","op_type":"D","op_ts":"2018-05-23 10:31:43.000107","current_ts":"2018-05-23T10:36:48.875000","pos":"00000000000000001376","before":{"ID":1}}

显然,Oracle的数据已准实时同步到Kafka,格式为json,其中op_type代表操作类型,这个可配置,我没有配置则按默认的来,默认为

1             
2
3
gg.handler.kafkahandler.format.insertOpKey = I             
gg.handler.kafkahandler.format.updateOpKey = U
gg.handler.kafkahandler.format.deleteOpKey = D

before代表操作之前的数据,after代表操作后的数据,现在已经可以从kafka获取到同步的json数据了,后面可以用SparkStreaming和Storm等解析然后存到hadoop等大数据平台里

6.4 SparkStreaming测试消费同步消息

具体代码可参考 Spark Streaming连接Kafka入门教程
下面附上消费成功的结果图

7、更新:后续遇到的问题

在后面的使用过程中发现上面同步到kafka的json数据中少一些我们想要的一些,下面讲一下我是如何解决的
首先建表:

1             
2
3
4
5
6
7
8
9
CREATETABLE"TCLOUD"."T_OGG2"             
("ID"NUMBER(*,0),
"TEXT_NAME"VARCHAR2(20),
"AGE"NUMBER(*,0),
"ADD"VARCHAR2(100),
"IDD"VARCHAR2(100),
CONSTRAINT"T_OGG2_PK"PRIMARYKEY("ID","IDD")

)

为什么不用之前建的表,主要是之前的字段太少,不容易看出问题,现在主要是增加几个字段,然后id,idd是联合主键。
看一下按照之前的配置,同步到kafka的数据(截取部分数据)

1             
2
3
4
5
{"table":"TCLOUD.T_OGG2","op_type":"I","op_ts":"2018-05-31 11:46:09.512672","current_ts":"2018-05-31T11:46:15.292000","pos":"00000000000000001903","after":{"ID":4,"TEXT_NAME":null,"AGE":0,"ADD":null,"IDD":"8"}}             
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 11:49:10.514549","current_ts":"2018-05-31T11:49:16.450000","pos":"00000000000000002227","before":{},"after":{"ID":4,"TEXT_NAME":"lisi","IDD":"7"}}
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 11:49:48.514869","current_ts":"2018-05-31T11:49:54.481000","pos":"00000000000000002373","before":{"ID":4,"IDD":"7"},"after":{"ID":1,"IDD":"7"}}

{"table":"TCLOUD.T_OGG2","op_type":"D","op_ts":"2018-05-31 11:52:38.516877","current_ts":"2018-05-31T11:52:45.633000","pos":"00000000000000003161","before":{"ID":1,"IDD":"7"}}

现在只有insert的数据是全的,update更新非主键字段before是没有数据的,更新主键before只有主键的数据,delete只有before的主键字段,也就是update和delete的信息是不全的,且没有主键信息(程序里是不能判断哪一个是主键的),这样对于程序自动解析同步数据是不利的(不同的需求可能不一样),具体自己可以分析,就不啰嗦了,这里主要解决,有需要before和after全部信息和主键信息的需求。

7.1 添加before

在源端extract里添加下面几行

1             
2
3
4
GGSCI (ambari.master.com) 33> edit param extkafka             
GETUPDATEBEFORES
NOCOMPRESSDELETES
NOCOMPRESSUPDATES

重启 extkafka

1             
2
stop extkafka             
start extkafka

然后测试

1             
2
3
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 14:48:55.630340","current_ts":"2018-05-31T14:49:01.709000","pos":"00000000000000003770","before":{"ID":1,"AGE":20,"IDD":"1"},"after":{"ID":1,"AGE":1,"IDD":"1"}}             
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 14:48:55.630340","current_ts":"2018-05-31T14:49:01.714000","pos":"00000000000000004009","before":{"ID":1,"AGE":20,"IDD":"2"},"after":{"ID":1,"AGE":1,"IDD":"2"}}
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 14:48:55.630340","current_ts":"2018-05-31T14:49:01.715000","pos":"00000000000000004248","before":{"ID":1,"AGE":20,"IDD":"8"},"after":{"ID":1,"AGE":1,"IDD":"8"}}

发现update之后before里有数据即可,但是现在before和after的数据都不全(只有部分字段)

网上有的说只添加GETUPDATES即可,但我测试了没有成功,关于每个配置项什么含义可以参考 https://blog.csdn.net/linucle/article/details/13505939(有些配置的含义里面也没有给出)
参考: http://www.itpub.net/thread-2083473-1-1.html

7.2 添加主键

在kafka.props添加

1            
gg.handler.kafkahandler.format.includePrimaryKeys=true            

重启 rekafka

1             
2
stop rekafka             
start rekafka

测试:

1             
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 14:58:57.637035","current_ts":"2018-05-31T14:59:03.401000","pos":"00000000000000004510","primary_keys":["ID","IDD"],"before":{"ID":1,"AGE":1,"IDD":"1"},"after":{"ID":1,"AGE":20,"IDD":"1"}}             

发现有primary_keys,不错~

参考: http://blog.51cto.com/lyzbg/2088409

7.3 补全全部字段

如果字段补全应该是Oracle没有开启全列补充日志

1             
2
3
4
5
SQL> select supplemental_log_data_all from v$database;             

SUPPLE
------
NO

通过以下命令开启

1             
2
3
4
5
6
7
8
9
10
11
SQL> alter database add supplemental log data(all) columns;             

Database altered.

SQL> select supplemental_log_data_all from v$database;

SUPPLE
------
YES

SQL>

测试一下

1             
2
3
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 15:27:45.655518","current_ts":"2018-05-31T15:27:52.891000","pos":"00000000000000006070","primary_keys":["ID","IDD"],"before":{"ID":1,"TEXT_NAME":null,"AGE":1,"ADD":null,"IDD":"1"},"after":{"ID":1,"TEXT_NAME":null,"AGE":20,"ADD":null,"IDD":"1"}}             
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 15:27:45.655518","current_ts":"2018-05-31T15:27:52.893000","pos":"00000000000000006341","primary_keys":["ID","IDD"],"before":{"ID":1,"TEXT_NAME":null,"AGE":1,"ADD":null,"IDD":"2"},"after":{"ID":1,"TEXT_NAME":null,"AGE":20,"ADD":null,"IDD":"2"}}
{"table":"TCLOUD.T_OGG2","op_type":"U","op_ts":"2018-05-31 15:27:45.655518","current_ts":"2018-05-31T15:27:52.895000","pos":"00000000000000006612","primary_keys":["ID","IDD"],"before":{"ID":1,"TEXT_NAME":null,"AGE":1,"ADD":null,"IDD":"8"},"after":{"ID":1,"TEXT_NAME":null,"AGE":20,"ADD":null,"IDD":"8"}}

到现在json信息里的内容已经很全了,基本满足了我想要的,附图:

启发我发现和Oracle全列补充日志没有开启有关的博客: https://blog.csdn.net/huoshuyinhua/article/details/79013387
开启命令参考: https://blog.csdn.net/aaron8219/article/details/16825963

注:博客上讲到,开启全列补充日志会导致磁盘快速增长,LGWR进程繁忙,不建议使用。大家可根据自己的情况使用。

8、关于通配

如果想通配整个库的话,只需要把上面的配置所有表名的改为 ,如test_ogg.test_ogg改为 test_ogg.,但是kafka的topic不能通配,所以需要把所有的表的数据放在一个topic即可,后面再用程序解析表名即可。

9、附录

目标端在 这里,下载下来后文件名123111_ggs_Adapters_Linux_x64.zip

源端在 旧版本查询下载,下载后文件名为V34339-01.zip

参考资料

基于OGG的Oracle与Hadoop集群准实时同步介绍

本文由  董可伦 发表于  伦少的博客 ,采用 署名-非商业性使用-禁止演绎 3.0进行许可。

非商业转载请注明作者及出处。商业转载请联系作者本人。

本文标题: 利用ogg实现oracle到kafka的增量数据实时同步

本文链接: https://dongkelun.com/2018/05/23/oggOracle2Kafka/


如何优雅设计 API 接口,实现统一格式返回?

$
0
0

【编者的话】在移动互联网,分布式、微服务盛行的今天,现在项目绝大部分都采用的微服务框架,前后端分离方式。


题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。
一般系统的大致整体架构图如下:
1.jpeg

需要说明的是,有些小伙伴会回复说,这个架构太简单了吧,太low了,什么网关啊,缓存啊,消息中间件啊,都没有。因为老顾这篇主要介绍的是API接口,所以我们聚焦点,其他的模块小伙伴们自行去补充。

接口交互

前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。

针对URL路径的restful风格,以及传入参数的公共请求头的要求(如:app_version,api_version,device等),这里就不介绍了,注重介绍一下后端服务器如何实现把数据返回给前端。

返回格式

后端返回给前端我们一般用JSON体方式,定义如下:
{  
#返回状态码
code:integer, 
#返回信息描述
message:string,
#返回值
data:object


CODE状态码

code返回状态码,一般小伙伴们是在开发的时候需要什么,就添加什么。

如接口要返回用户权限异常,我们加一个状态码为101吧,下一次又要加一个数据参数异常,就加一个102的状态码。这样虽然能够照常满足业务,但状态码太凌乱了。

我们应该可以参考HTTP请求返回的状态码。

下面是常见的HTTP状态码:
  • 200 – 请求成功
  • 301 – 资源(网页等)被永久转移到其它URL
  • 404 – 请求的资源(网页等)不存在
  • 500 – 内部服务器错误


2.jpeg

我们可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4位数。
  • 1000~1999 区间表示参数错误
  • 2000~2999 区间表示用户错误
  • 3000~3999 区间表示接口异常


这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位。

Message

这个字段相对理解比较简单,就是发生错误时,如何友好的进行提示。一般的设计是和code状态码一起设计,如:
3.png

再在枚举中定义,状态码。
4.jpeg

状态码和信息就会一一对应,比较好维护。

Data

返回数据体,JSON格式,根据不同的业务又不同的JSON体。

我们要设计一个返回体类Result。
5.jpeg

控制层Controller

我们会在controller层处理业务请求,并返回给前端,以order订单为例:
6.jpeg

我们看到在获得order对象之后,我们是用的Result构造方法进行包装赋值,然后进行返回。小伙伴们有没有发现,构造方法这样的包装是不是很麻烦,我们可以优化一下。

美观优化

我们可以在Result类中,加入静态方法,一看就懂。
7.jpeg

那我们来改造一下Controller。
8.jpeg

代码是不是比较简洁了,也美观了。

优雅优化

上面我们看到在Result类中增加了静态方法,使得业务处理代码简洁了。但小伙伴们有没有发现这样有几个问题:
  1. 每个方法的返回都是Result封装对象,没有业务含义。
  2. 在业务代码中,成功的时候我们调用Result.success,异常错误调用Result.failure。是不是很多余。
  3. 上面的代码,判断id是否为null,其实我们可以使用hibernate validate做校验,没有必要在方法体中做判断。


我们最好的方式直接返回真实业务对象,最好不要改变之前的业务方式,如下图:
9.jpeg

这个和我们平时的代码是一样的,非常直观,直接返回order对象,这样是不是很完美。那实现方案是什么呢?

实现方案

小伙伴们怎么去实现是不是有点思路,在这个过程中,我们需要做几个事情:
  1. 定义一个注解@ResponseResult,表示这个接口返回的值需要包装一下
  2. 拦截请求,判断此请求是否需要被@ResponseResult注解
  3. 核心步骤就是实现接口ResponseBodyAdvice和@ControllerAdvice,判断是否需要包装返回值,如果需要,就把Controller接口的返回值进行重写。


注解类

用来标记方法的返回值,是否需要包装。
10.png

拦截器

拦截请求,是否此请求返回的值需要包装,其实就是运行的时候,解析@ResponseResult注解。
11.jpeg

此代码核心思想,就是获取此请求,是否需要返回值包装,设置一个属性标记。

重写返回体

12.jpeg

上面代码就是判断是否需要返回值包装,如果需要就直接包装。这里我们只处理了正常成功的包装,如果方法体报异常怎么办?处理异常也比较简单,只要判断body是否为异常类。
13.jpeg

怎么做全局的异常处理,篇幅原因,老顾这里就不做介绍了,只要思路理清楚了,自行改造就行。

重写Controller

14.jpeg

在控制器类上或者方法体上加上@ResponseResult注解,这样就ok了,简单吧。到此返回的设计思路完成,是不是又简洁,又优雅。

总结

这个方案还有没有别的优化空间,当然是有的。如:每次请求都要反射一下,获取请求的方法是否需要包装,其实可以做个缓存,不需要每次都需要解析。当然整体思路了解,小伙伴们就可以在此基础上面自行扩展。

原文链接: https://segmentfault.com/a/1190000021653916

    推荐21部好看的纪录片

    $
    0
    0



    按:纪录片是土摩托老师长期关注的领域,他曾在博客里开辟专栏推荐各类精品纪录片,这个片单是从他看过的海量纪录片中淘选出来的。春节假期,肺炎病毒流行。如果你不打算出门的话,那就腾出点时间看看纪录片吧。


     

    自然科学类

     

    01,BBC- Seven Worlds One Planets(《七个世界一个星球》)

     


    BBC是自然类纪录片的鼻祖,大卫·爱登堡(David Attenborough)爵爷是这类片子最好的主持人,凡是他们合作出的片子,不用多问,找来看就是了。


    如果你从来没有看过这类片子,那么可以从这套新片看起。BBC的拍片风格发生了很大变化,明显增加了故事性和戏剧性,希望能吸引那些以前从来不看这类片子的普通观众。

     

    PS:当然这种增加是有科学根据的,不是盲目地将动物拟人化,这一点我曾经当面问过爵爷,得到了相当坚决的回答。换句话说,这里的戏剧化是通过高超的剪辑和配乐来实现的,不是《走近科学》那种伪科学式的瞎搞。

     


    02,BBC- The Joy of AI(《人工智能的喜悦》)

     

     

    光头大叔吉姆·阿尔卡利里(Jim Al-Khalili)是我第二喜欢的BBC纪录片主持人,他是个伊拉克出生英国长大的理论物理学家,聪明自不必说,主持风格我也特别喜欢。凡是他主持的片子,不用多问,找来看就是了。


    这是他新出的一部纪录片,在我看过的所有关于人工智能(AI)的纪录片里,这部绝对排名第一,主要就是因为主持人的理性态度。

     

    PS:举个例子:阿尔卡利里相信AI的飞速发展对于人类来说应该是一个狂喜,这个结论的背后是有坚实的逻辑和证据支持的,可惜大部分媒体报道都偏重AI的潜在危险,实在有点短视了。

     


    03,PBS- How We Got to Now-With Steven Johnson(《我们是如何走到今天这一步的》)

     

     

    史蒂文·约翰逊(Steven Johnson)是我非常喜欢的一位美国科普作家,这是他和BBC合作拍摄的一个六集纪录片(首播于PBS电视台),讲述影响人类生活方式的六个发明的前因后果,包括城市公共卫生、计时器、玻璃、人造光、制冷设备和声音记录和重现,都是一些我们已经习以为常,但其实不久前人类还无福消受的新发明。

    这套片子最有意思的地方在于他讲述的故事和人物全都是大家不怎么熟悉的,即使遇到像爱迪生这样绕不过去的人物也都一笔带过,因为他发现影响人类生活的这六大发明很大程度上都是集体智慧的结晶,很多小人物起到了更加关键的作用。

    该片另一大特点就是八卦性很强,约翰逊知识面极为广泛,善于把很多看似不相干的事件联系到一起,让人脑洞大开,比如你们能否想到制冷技术的进步竟然帮助里根当选了美国总统吗?

     

    PS:看完这套片子我忍不住想,如今流行知识付费,中国人花了很多钱去听一些不靠谱的中国学者解读世界大事,其实如果你学点英语,放宽眼界的话,你会发现真正好玩又靠谱的知识产品在国外早就有了,比如这套片子就是。大部分中国观众不知道这些好东西的存在,白花了很多冤枉钱,浪费的却是自己宝贵的时间,太可惜了。

     


    04,BBC- The Brain: A Secret History(《大脑的秘密历史》)

     

     
    麦克·莫斯利(Michael Mosley)也是我非常喜欢的一位BBC纪录片主持人,这套三集纪录片详细梳理了人类探索大脑功能的历史,特别好看。

    众所周知,大脑是最难研究的器官,主要原因是很难做实验。以前科学伦理还不那么完备,有人花钱找来志愿者做了几个不道德的实验,早期关于大脑的一些基本理论就是这么来的。后来不能这么做了,科学家便只能寄希望于手术导致的意外。

    第三集讲了几个这样的案例,简直超出了一般人的想象。比如,人类大脑有些部位目前看来似乎没什么用,但切掉后对于这个人几乎没有任何影响。再比如,有位医生曾经像吸酸奶一样在手术时用吸管吸掉了一个病人的海马区,结果这个人手术之后便失去了形成记忆的能力,所有事情一秒钟后就忘了。 PS:这个人是心理学历史上最著名的研究对象,在他身上一共发表过超过一万篇论文,几乎每天都有科学家跑来研究他。不过他也不烦,因为他全都忘记啦! 

    05,PBS NOVA- Apollo's Daring Mission(《阿波罗计划》) 


    美国PBS电视台在2018年底播出了一集NOVA纪录片,回顾了阿波罗8号的整个过程,看得我惊心动魄,老泪纵横。

    原来阿波罗8号原本的计划是测试土星5号火箭在地球轨道上的表现,当时这枚火箭只成功地发射过一次。CIA的间谍卫星发现苏联人貌似也有了一枚体型巨大的火箭,因此NASA怀疑苏联人要赶在美国人前面去往月球,于是NASA临时改变计划,把阿波罗8号派去了月球。

    绕月飞行所需的整套系统都没有经过实地测试,三名勇敢的宇航员就出发了。所幸一切均好,任务圆满完成。阿波罗8号是人类第一次离开地球轨道,宇航员拍摄的“地升”照片是上世纪人类拍摄到的最重要的一张照片,没有之一。这一切都发生在整整半个世纪前,阿波罗计划太伟大了! PS:我在《三联生活周刊》上写过一篇文章,标题叫做《阿波罗计划的遗产》,大家可以找来读读。 

    06,PBS- Serengeti Rules(《塞伦盖提法则》)  这是美国PBS电视台去年播出的一部自然历史类纪录片,通过采访真人和演员表演相结合的方式,讲述了五位生态学家在各自的领域中发现了一个新的生态法则的故事。

    这个法则源自1960年代在潮汐水坑里做的一个小实验,颠覆了以往关于生态系统规模限制的旧理论。按照这个新法则,决定一个生态系统健康的关键因素不是基础生产者(海草或者植被),而是某个关键物种(Keystone Species)的多寡,比如海星或者角马。

    这里所说的关键物种大都为顶级捕食者(但并不绝对),后者正是人类最喜欢捕捉的物种(因为它们的经济价值往往更高),但这么做对生态系统的伤害巨大。同理,如果我们想要恢复一个被破坏的生态系统,引入一个关键物种也许是更好的方法。 PS:这个理论有年头了,但对于普通民众来说可能还挺新鲜的,而且理解起来也不困难,值得补课。这部片子的拍摄手法很棒,很容易激发起观众对大自然的热爱,所以更应该推荐给小朋友们看看。 

    07, AlphaGo(《阿尔法围棋》)   
    这片子回顾了当年谷歌阿法狗和李世石的那场世纪大战的前前后后,有很多针对当事人的采访,看得人惊心动魄!

    当年那场人机大战开启了人工智能新热潮,让普通老百姓也都意识到AI时代已经到来。我甚至认为,未来的历史学家们将会把那场大战当做是人类新纪元的开始,所以这部史诗般的纪录片极具收藏价值。 PS:很多人相信阿法狗的胜利是人类的失败,可你别忘了,阿法狗也是人类造出来的!所以这件事从头到尾都是人类的胜利,各位千万别受那帮悲观主义者的蛊惑,忘记了人类的力量才是无穷的。 
    08,BBC- Deep Down and Dirty-The Science of Soil(《土壤的科学》)   
    这是BBC拍的一部纪录片,也许没有那么好看,但题材非常重要,值得推荐。

    土壤很可能是最不受重视的自然资源,但人类的生活绝对离不开土壤,农业只是原因之一。这部片子不但解释了土壤的起源,还解释了土壤和碳排放的关系,告诉我们化石能源绝不是温室气体排放的唯一来源。 PS:片子最后还指出了减少土壤损失的方法,那就是减少裸土。这一点在中国非常不受重视,不信你出去走走,你会发现中国城市到处都是裸土,因为中国人还有很强的农民心态,就连住宅小区的花园苗圃都要除草翻耕,思维意识实在是太落后了。
     
    09,PBS- The Eugenics Crusade(《优生学历史》)
     
    如今一提优生学,很多人首先想到的是纳粹的种族灭绝大屠杀,但优生学的起源是在英国,这个名词的发明人是达尔文的表弟。最早继承这一思想的则是美国人,当年很多州都通过了相关法律,允许政府给一些“不适合当父母”的人提前实施绝育手术。 优生学这个概念听起来特别合理,甚至是美好的。但当年的遗传学研究刚刚起步,一些人在还没有搞清楚原理的情况下就提前实施了,结果当然是灾难性的。但是,如果大家因此就认为所有新技术都要等上三代才能应用,这也不一定就是正确的做法。这很可能是个无解的问题,人类就是这样曲曲折折地朝前走的。 格外需要提醒大家的是,最早反对优生学的也正是科学家群体,尤其是当研究果蝇的摩尔根发现了遗传的复杂性之后,反对的声音才变得有理有据。这件事充分说明了科学和政治不一样的地方:科学有很强的自我纠错机制,政治则要弱得多,政治上的纠错往往需要让一代人吃尽了苦头才能成功。 PS:如今因为纳粹的关系,优生学成了禁区,但人类对于优生的渴望其实一直都存在,如今越来越流行的产前检测就是明证。所以,这件事背后的伦理道德其实是非常复杂的,任何简单的解释都很难讲得清楚。 

    10,BBC- How to Go Viral(《病毒式传播》)   
    米姆(Meme)是著名的英国科普作家理查德·道金斯(Richard Dawkins)在《自私的基因》这本书里首次创造出来的概念,后经苏珊·布莱克摩尔(Susan Blackmore)发展,成为描述文化传播现象的基本理论之一。

    总部位于英国的DRG(Dream Reality Group)电视节目制作公司2019年制作了一部关于Meme的新片,主持人理查德·格雷(Richard Clay)不但请来了道金斯和布莱克摩尔现场说法,还采访了大量幕后人士,试图解释互联网时代Meme传播的各种猫腻,值得一看。片中展示了各种Meme是如何被人为地制造出来的,相信你会对如今的互联网多一分警惕。 PS:看完后我的感想是:旧的Meme理论已经无法解释互联网时代的病毒式营销了,需要一个新的替代理论,我推荐过的那本《无穷的开始》对Meme给出了很不错的解释,比如Meme是如何毁掉传播Meme的社会的,以及Meme是如何让一个社会保持静态的,等等。

    建议大家读读那本书里关于Meme的章节,想办法突破Meme为我们制造的思想牢笼,让Meme更好地为人类社会服务。 

    11,BBC- Climate Change(《气候变化》)   
    这是BBC One频道2019年拍的新片,几乎涉及到了气候变化的方方面面,如果你觉得自己对这个话题了解得不够系统,那么值得一看。

    印象中这是爱登堡爵爷第一次单独为气候变化站台,就为这个也值得一看。这个话题已经热过好几轮了,最近因为那个瑞典女中学生又在欧洲火了一把,不知能延续多长时间。 PS:我做了十多年记者,科普方面花心思最多的除了转基因就是气候变化,可惜这两个话题可以说都是收效甚微,原因各有不同。气候变化议题难度最大,因为它除了事关公民的科学素养,还涉及到经济制度之争。

    包括同人于野在内的很多科普作家也是在这个问题上栽了跟头,像他那样的书呆子科普作家在中国还有不少,居然还都挺火,真是灾难。 

    人文历史类  01,PBS- The Vietnam War(《越战》)   
    这是美国PBS电视台的鸿篇巨制,总投资三千万美元,前后拍了十年,最终总片长接近二十个小时,信息密度非常大。 越战的重要性不必多说,历史会有评价。美国人反思越战的各种作品出过无数,几乎没有任何禁区,这一点真是令人羡慕,再加上美国政府的保密法规定五十年后所有政府秘密必须公开,所以最近几年反思的力度非常大,这部片子就是在这种背景下拍出来的。 越战大概是第一个从头到尾从里到外都有大量图片和视频的战争,编导拿到了两万四千张照片,以及一千五百小时的视频资料,包括美国军方拍摄的二十四场真实战斗的视频,甚至还有北越方面拍摄的很多视频素材。再加上解密后大量美国政府高官的秘密通话被公开,所以这片子是极为难得的一个对于一场国际战争的全景描述,而且极为真实。 我看的时候一直试图让自己穿越到半个多世纪前,想象自己生活在那个时代,接受的都是那个时代特有的信息,然后再来推测自己会有怎样的感觉和判断,我的立场很快就变模糊了,很多自己曾经深信不疑的观点也都被动摇了。 这才是最顶级的历史纪录片应该有的样子,它会帮助你更好地了解历史事件到底是如何发生的,为什么当时的人看上去都那么“傻”,只有这样才能更好地看清当下,预测未来。 如今流行十万+,于是我们的很多时间都被那些依照算法炮制出来的公号文章所占用,腾不出时间去看这种含金量超高,但却不那么容易消化的作品了,甚是遗憾。 PS:这部片子的配乐超赞,背景音乐来自马友友,配歌来自那个时期的热门歌曲,几乎可以说是六十年代美国流行音乐的精选集。 

    02,Netflix- Wild Wild Country(《异狂国度》) 
    这是《越战》级别的长篇历史纪录片,讲述了1980年代奥修真理教在美国俄勒冈州建立公社的详细经过。

    该片最大的优点在于平衡,导演采访了各方人士,给每个人一个平等的发言机会。如果你想知道宗教是怎如何诞生的,美国的民主和法制又是怎么一回事,政教分离到底意味着什么,或者你对洗脑、公社、宽容、物质与精神、心灵自由、乌托邦……等等这些概念感兴趣的话,这部纪录片都会帮助你距离真相更近一步。 PS:这件事距今有些年头了,估计很多年轻人都不知道有这回事了。如果你是这样的人,那么我建议你先别去谷歌这个事情,而是先看片再上网调研,这么做将会极大地提升这部片子的力量。 

    03,SundanceTV- Jonestown: Terror in the Jungle(《琼斯镇:丛林恐怖》)   
    这是圣丹斯电视台2018年首映的四集纪录片,我再次被震撼了。人民圣殿教的新闻发生在1978年,我有残存的一点记忆,看完这部片子,那段记忆重新被唤起。

    和奥修真理教不同的是,人民圣殿教最后的结局是教主率领手下九百多名教众集体服毒自杀,无论从哪个角度看都是负面事件,所以这部纪录片没有采访太多对这个教持正面态度的人,对于那些凡事喜欢从正反两个角度看问题的人来说会有些不爽。

    即便如此,因为这件事的结局太过离奇,这个故事的惊悚程度一点也不亚于上面那部关于奥修真理教的纪录片,其教育意义甚至更大。 这个故事会让你再次扪心自问:宗教和邪教的区别在哪里?“宗教自由”到底意味着什么?这个口号究竟是不是个虚伪的神话?以及,人类究竟能蠢到何种程度,能让这样一个手段相当拙劣的洗脑者获得如此巨大的成功? 

    04,PBS- The Story of Fascism in Europe(《欧洲法西斯的故事》) 

    里克·史迪威斯(Rick Steves)是PBS电视台的旅行记者和欧洲问题专家,这是他在2017年为PBS制作的一部关于法西斯主义的入门级纪录片,强烈建议那些对这个词不甚了解的朋友看一看。

    我自己就学到了很多新知识,比如Fascism这个词竟然来源于拉丁语的Fasces,意思是“束棒”,就是上图中他手里拿着的这个东西。中国不是有个流传极广的民间故事吗?说一根筷子容易折断一把筷子很难折断,意思是一样的。换句话说,法西斯主义的本意就是“团结就是力量”! 按照这个片子的说法,法西斯的出现需要以下三个因素:一个对本民族文化传统自视甚高的劳动人民群体;因为经济或者政治等原因导致的挫败感;以及一个张口闭口离不开“人民”和“爱国主义”的民粹领袖。大家可以好好琢磨一下,看看当今世界距离这三个因素还差多少。 PS:这部片子详细分析了当年的德国和意大利的内部情况,很多细节让我感觉是如此熟悉,每每感到脊背发凉,冷汗直冒。 

    05, American Factory(《美国工厂》)   
    这可能是我最近一段时间看过的最好看的社会类纪录片,光是找到这个选题并说服双方允许自己拍摄就成功了一半。

    这片子相当中立,双方各给了一半时间,这么做容易让每位“有立场”的观影者都有一半的时间感到不爽,但“让你不那么爽”才是社会类纪录片的精髓。

    这片子剪得非常密,几乎每隔五分钟就有一个让你拍案叫绝的“点”。我对两个比较隐蔽的点印象深刻,一个是美国工厂开工庆典,美方需要考虑下雨怎么办,曹德旺说,不会下雨的。另一个是美方管理人员去中国参观,一个美国胖子穿不进中方准备的工作服。 PS:我在中美两国都生活过很长时间,甚至在俄亥俄生活过四年,对片中的场景深有体会。看完后我愈发相信马克思关于人类社会发展的五阶段理论是正确的,任何一个阶段都是没法省略的,尤其是资本主义阶段。我前段时间还讲过民歌在工会运动中的作用,人家在一百多年前就闹工会,而且闹了特别长的时间,对美国社会产生了深远的影响。咱们没有真正闹过,只是拿工会当政治工具而已,双方没办法相互理解。 

    06, The Edge of Democracy(《民主的边界》)   
    这部片子刚刚获得了今年的奥斯卡最佳长纪录片提名,导演是巴西人,家里曾经是左派游击队成员,因此和巴西前总统卢拉(Lula)关系很不错,获得了无限接近Lula的权利,拍摄了大量幕后镜头。

    这部片子以个人视角讲述了Lula所在的左派劳工党最近这二十年里在巴西的遭遇,看完后简直要对人类这个物种彻底绝望了。原来民主政治还可以这么肮脏,老百姓还可以这么愚昧。 PS:这部电影好玩的地方在于,Lula和他一手提拔的巴西女总统Dilma最终是被巴西国内的保守势力运用政治手段打压的,后者更是以一种莫须有的罪名被弹劾。但与此同时,在美国正在上演相反的桥段,特朗普是当权的保守派,美国的自由派正在以各种原因试图弹劾他,两国互为镜像。

    最终,巴西选民选出了一个保守派,几乎可以称之为“南美洲的特朗普”,不知道今年的美国大选会是什么结果。总之,导演把巴西的失败当成民主的失败,这是不公平的。最近几年民主本身一直赢,但老百姓就不好说了,所以民主制度会不会是人类的未来?这个问题还值得问么? 

    07,CNN- The History of Comedy(《喜剧的历史》) 
    这是CNN拍的八集纪录片,值得推荐给所有打算了解美国文化的人。

    提起美国对世界文化的贡献,首推音乐领域,布鲁斯爵士乐摇滚乐迪斯科嘻哈……光是美国发明的音乐类型就可以列出一长串。排名第二的恐怕就是喜剧,我们每个人都通过好莱坞电影认识了至少十个美国喜剧明星,他们大都是通过脱口秀这个平台走向世界的。

    这套片子以美式单口为基础,详细梳理了美国脱口秀的发展史,大家可以看看这东西是如何从遍布各地的小俱乐部里慢慢发展起来的。 PS:美国音乐的兴旺必须感谢这个国家的移民,脱口秀的兴旺则和移民关系不大,而是源于美国这个大市场,以及英语的普及。其他西方发达国家也有自己的喜剧,但因为语言障碍或者国内市场太小,群众基础要比美国薄弱得多,所以一直发展不起来。 

    08,Netflix- Rolling Thunder Revue(《滚雷巡演》) 
    1975年,美国开始流行在体育场举办大型摇滚演出,已经好多年没有巡演的迪伦却决定模仿过去的马戏团,组织一帮朋友在美国东部的中小型城市举办一系列几千人规模的体育馆演出,取名“滚雷巡演”。

    他招来十几个好朋友,坐着一辆大巴车,开始了这次奇怪的演出。同行的除了伴奏乐队The Band的几名成员,还有Joan Baez, Joni Mitchell, Allen Ginsberg和The Byrds乐队的Roger McGuinn,以及刚刚被招募来的暗黑系小提琴手Scarlet Rivera。

    迪伦还雇了一个摄影团队,把整个巡演拍了下来,准备剪成一部电影,这就是后来那部长达四小时的 Renaldo and Clara,最终巡演赔了一大笔钱,电影几乎没人看,所以这件事很快就被人遗忘了。好在Netflix没有忘,找来了最擅长拍迪伦的马丁·斯科西斯(Martin Scorsese),从浩如烟海的视频资料中剪出了这部两个半小时的纪录片。 如果你是迪伦迷,此片必看!因为片子里展示了迪伦最好的舞台状态,像A Hard Rain’s A-Gonna Fall,Hurricane和Just Like a Woman这几首歌的现场表演都非常棒,尤其是那首One More Cup of Coffee,舞台上的迪伦像是中了魔,太神奇了!

    最奇怪的是,当年的那次巡演因为要拍电影,所以很多音乐家都画了浓妆,甚至戴了面具,很多情节真真假假,分不清哪个是真实哪个是表演。斯科西斯如法炮制,在这部片子里也虚构了好几个人物,用电影技术将其混在片中,所以我们看的时候其实也不知道哪个是真的,哪个是假的。就像迪伦在片子一开始时不戴面具说的:一个人戴上面具才会说真话,不戴面具时说的很可能就是假话。 迪伦在片子中还说了一句非常经典的话:Life isn't about finding yourself, life is about creating yourself(生活不是为了寻找你自己,而是为了创造你自己)。这句话就是迪伦一生的真实写照。 PS:这部片子不但有大量演出实况,还有很多后台拍摄的音乐家开派对的情景,这才是精华所在。我认为这是迪伦理想中的生活状态,所以片子结尾列出了1975年之后迪伦所有的演出时间地点,你会看到他此后几乎全都生活在路上了,这才是一名民歌手应该有的生活。 

    09, Marianne & Leonard: Words of Love(《玛丽安和伦纳德:爱的语言》)   
    这是2019年新出的一部传记类纪录片,伦纳德·科恩(Leonard Cohen)的歌迷必看。

    该片讲述了科恩和玛丽安(Marianne)之间长达几十年的友谊和爱情,后者是科恩早年的缪斯,两首重要歌曲So Long Marianne和Bird on a Wire的灵感就是源于两人在希腊Hydra岛上的生活。具体细节不多说了,大家可以去看我写的那本《来自民间的叛逆》。 这部片子除了各种相关人士的访谈之外(很多访谈非常精彩),最值回票价的就是各种珍贵视频,包括当年Hydra岛上艺术家聚会的视频,科恩回到纽约后第一次公开演出的视频,科恩成名后和女歌迷调情的视频(这个居然都有!),玛丽安离开科恩后独立生活的视频,科恩那次著名的耶路撒冷演出的视频,科恩当和尚的视频,科恩复出后世界巡演的视频,(包括科恩在奥斯陆唱《So Long Marianne》时坐在第一排的玛丽安的反应),以及玛丽安临死前科恩委托朋友朗诵自己写给她的一封信时的情景。熟悉这段历史的我看到这些珍贵视频时,心里真是五味杂陈啊! PS:女权主义者肯定会觉得科恩就是渣男,两人之间的感情也肯定会让很多传统人士感到不适,我自己其实也说不好应该如何评价这段感情,但作为科恩的歌迷,我肯定更关心科恩这一方的故事。没办法,这是人之常情。唯一让我感到不适的是玛丽安的儿子Axel,长大后居然疯了,这就是成长于那段疯狂年代的儿童最终必须付出的代价。 

    10,Netflix- Ugly Delicious(《美食不美》) 
    这是我看过的最好看的美食纪录片。我看的这类片子不算多,大都是隐性的美食广告,顶多加点旅游元素,本质上也是广告。《舌尖上的中国》和《风味人间》略有不同,陈晓卿利用美食来讲述中国人的生存故事,外延非常宽,所以才会好看。
     但是,这套片子太不一样了,它用美食来讲政治、讲文化、讲历史、讲人性……而且是用采访对象自己的话来讲,而不全是主持人或者画外音,这一点极为难能可贵。另外,这部片子走访了全世界的很多地方,这也是中国目前很难做到的。 当然了,这套片子的成功,一大半原因要归功于主持人David Chang,他是个韩裔美国人,讲话的方式非常幽默,性格也很讨人喜欢,是那种我特别想跟他一起吃饭一起旅行一起Hang Out的那种哥们儿。我看这部片子的感觉很像是我在跟一群特别好玩又特别有见识的人边吃边聊,有点像老男人饭局,这种感觉我太熟悉了。 PS:我觉得目前中国还拍不出这样的美食纪录片,一来中国的大环境不允许进行这种无拘无束的讨论,二来中国观众大概也没有准备好接受这种包容一切的世界观。第三,我有一种奇怪的感觉,那就是如果这部片子是中国人用中文在讲话,一定不如英文精彩,因为中文一听就有种地方语言的Feel,不像英文,本身就有一种包容一切的感觉。

    当然我的感觉不一定对,还是真心希望有中国人能做出接近这种水平的美食纪录片吧。

    相关阅读:
    108部高分纪录片,陪娃一起过寒假

    本文作者:袁越(土摩托)


    Kafka参数影响及性能测试_tom_fans的博客-CSDN博客

    $
    0
    0

    Kafka提供了2个测试脚本,kafka-producer-perf-test.sh以及kafka-consumer-perf-test.sh,  kafka参数非常多,有些使用默认即可,有些对性能影响极大,只有经过测试,你才能够对这些参数有直观的感觉。 下面我们先测试producer.

    先看看producer脚本怎么使用:

    [hdfs@namenode02 tmp]$  /opt/cloudera/parcels/KAFKA/lib/kafka/bin/kafka-producer-perf-test.sh
    usage: producer-performance [-h] --topic TOPIC --num-records NUM-RECORDS --record-size RECORD-SIZE --throughput THROUGHPUT
                                --producer-props PROP-NAME=PROP-VALUE [PROP-NAME=PROP-VALUE ...]
    
    This tool is used to verify the producer performance.
    
    optional arguments:
      -h, --help             show this help message and exit
      --topic TOPIC          produce messages to this topic
      --num-records NUM-RECORDS
                             number of messages to produce
      --record-size RECORD-SIZE
                             message size in bytes
      --throughput THROUGHPUT
                             throttle maximum message throughput to *approximately* THROUGHPUT messages/sec
      --producer-props PROP-NAME=PROP-VALUE [PROP-NAME=PROP-VALUE ...]
                             kafka producer related configuaration properties like bootstrap.servers,client.id etc..
    [hdfs@namenode02 tmp]$

    默认测试命令如下, 发送100000条记录,每个记录100 bytes

    /opt/cloudera/parcels/KAFKA/lib/kafka/bin/kafka-producer-perf-test.sh --topic jlwang --num-records 1000000 --record-size 100 --producer-props  bootstrap.servers=datanode04.isesol.com:9092 --throughput 1000000

    由于默认参数没有去做修改,那么主要的几个参数如下:

    buffer.memory = 33554432              这个就是消息缓存,producer发消息默认先发给buffer

    block.on.buffer.full = false                如果发送的消息量太大,撑满了buffer怎么办? 我相信kafka会有清理 buffer的功能,但是如果即使清理也赶不到发送速度呢? 这个参数的

                                                                  意义就是如果出现这个情况,是堵塞发送,还是报错?

    request.timeout.ms = 30000

    acks = 1

    retries = 0

    max.request.size = 1048576

    linger.ms = 0                                    

    batch.size = 16384

    接下来我们主要测试 batch, buffer, ack, linger.ms的影响。


    默认:

    1000000 records sent, 288184.438040 records/sec (27.48 MB/sec), 574.34 ms avg latency, 918.00 ms max

    acks=all :

    1000000 records sent, 121212.121212 records/sec (11.56 MB/sec), 1566.87 ms avg latency, 2640.00 ms max latency

    acks=all, linger.ms=100ms :

    1000000 records sent, 128188.693757 records/sec (12.23 MB/sec), 1506.37 ms avg latency, 1960.00 ms max latency

    buffer.memory=100000 :

    1000000 records sent, 66427.527567 records/sec (6.34 MB/sec), 1.06 ms avg latency, 307.00 ms max latency

    batch.size=1, acks=1 :

    16669 records sent, 3333.8 records/sec (0.32 MB/sec), 2587.5 ms avg latency, 4303.0 max latency.

    随后报错:org.apache.kafka.common.errors.TimeoutException: Batch Expired   生产的数据速度远远超过发送速度,导致失败timeout,然后失败。


    其实已经不用测了,上面这几个参数对整个发送性能都有相当大的影响, 如果发送量很大,可以考虑增加buffer, batch.size, linger.ms的值,acks设置为1.  至于设置多大,坦白说我觉得给个double就行了,也不需要太大。 如果发送量不大,其实默认值kafka给的很不错,可以应付大部分系统。

    另外要提一点record.size也严重影响发送速度,生产上尽量避免太大的record.size, 看下面测试结果,我设置record.size=10000,速度严重不行

    24499 records sent, 4899.8 records/sec (46.73 MB/sec), 364.1 ms avg latency, 748.0 max latency.
    28500 records sent, 5700.0 records/sec (54.36 MB/sec), 346.4 ms avg latency, 742.0 max latency.
    28134 records sent, 5626.8 records/sec (53.66 MB/sec), 363.0 ms avg latency, 806.0 max latency.
    28037 records sent, 5607.4 records/sec (53.48 MB/sec), 362.7 ms avg latency, 821.0 max latency.
    23201 records sent, 4640.2 records/sec (44.25 MB/sec), 429.9 ms avg latency, 1088.0 max latency.
    17055 records sent, 3411.0 records/sec (32.53 MB/sec), 605.7 ms avg latency, 1361.0 max latency.
    21415 records sent, 4283.0 records/sec (40.85 MB/sec), 490.0 ms avg latency, 1019.0 max latency.
    26560 records sent, 5312.0 records/sec (50.66 MB/sec), 383.6 ms avg latency, 853.0 max latency.
    23193 records sent, 4638.6 records/sec (44.24 MB/sec), 446.7 ms avg latency, 1225.0 max latency.
    26156 records sent, 5231.2 records/sec (49.89 MB/sec), 387.6 ms avg latency, 1068.0 max latency.
    28024 records sent, 5604.8 records/sec (53.45 MB/sec), 372.2 ms avg latency, 855.0 max latency.
    27209 records sent, 5441.8 records/sec (51.90 MB/sec), 377.0 ms avg latency, 842.0 max latency.


    对于consumer就不做具体测试了,主要是因为影响参数没那么多,receive.buffer.bytes,auto.offset.reset,max.partition.fetch.bytes,fetch.min.bytes,isolation.level,max.poll.interval.ms,receive.buffer.bytes,request.timeout.ms  

    估计真正会设置的几个参数也就这个,其他基本都不太用。






    使用 Docker 和 Traefik v2 搭建 Phabricator

    $
    0
    0

    本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

    本文作者: 苏洋

    创建时间: 2020年02月02日 统计字数: 12171字 阅读时间: 25分钟阅读 本文链接: soulteary.com/2020/02/02/…


    使用 Docker 和 Traefik v2 搭建 Phabricator

    这篇文章躺在草稿箱里有一个多月了,恰逢最近一段时间远程协作需求,以及 Traefik v2 的升级,于是便有了这篇文章。

    如果你的团队也需要一个内部看板,Phabricator 是个不错的选择:能提供简单的任务管理、能提供工作看板、支持代码讨论、甚至能够让设计师也使用起来,当然还有它主打的代码审计 / Review和管理功能。

    写在前面

    最早接触它是在 2012 年,八年之后,这款工具的开源版本变的更加好用了。

    从开源仓库可以看到,社区版的代码一直在持续更新,而且现在还提供了 SaaS 版本,考虑到私密性和账户集成等定制要求,这里决定自建服务,并自行封装容器镜像。

    考虑到不是所有人都有定制需求,这里分别提供两个方案,Bitnami 的容器方案,和完全基于官方代码进行定制的容器方案。

    目前社区最新版是 《stable - Promote 2020 Week 5》 ,Bitnami 会定时从官方仓库中获取版本,并进行容器封装,仓库地址: github.com/bitnami/bit…,相比官方更新会稍有延时,但是基本不影响使用。

    准备数据库

    生产环境推荐使用云服务商提供的数据库,但是如果小规模使用,使用容器启动一个数据库示例也未尝不可。

    version: '3.7'
    
    services:
    
      mariadb:
        image: mariadb
        environment:
          - MYSQL_ROOT_PASSWORD=phabricator
          - MYSQL_DATABASE=phabricator
        command: --max_allowed_packet=33554432 --sql_mode="STRICT_ALL_TABLES" --local-infile=0
        ports:
          - 13306:3306
        networks:
          - traefik
        healthcheck:
          test: "mysqladmin --password=phabricator ping -h 127.0.0.1"
          timeout: 5s
          retries: 20
        volumes:
          - './mariadb_data:/var/lib/mysql'
    
    networks:
      traefik:
        external: true
    复制代码

    将上面的内容保存为 docker-compose.yml并执行 docker-compose up -d即可。

    准备好数据库后,我们聊聊怎么简单启动一个 phabricator 服务。

    Bitnami 容器方案

    这里提供两个版本的配置文件,更多搭配 Traefik 使用的前置知识可以在 过往的文章中找到。

    搭配 Traefik v1 使用

    如果你还在使用 Traefik v1 ,那么使用下面的配置,可以一键启动封装好的稳定版本。

    version: '2'
    services:
    
      phabricator:
        image: 'bitnami/phabricator:2019'
        expose:
          - 80
          - 443
        volumes:
          - './phabricator_data:/bitnami'
          - './extensions:/opt/bitnami/phabricator/src/extensions'
        environment:
          - PHABRICATOR_HOST=phabricator.soulteary.com
          - PHABRICATOR_USERNAME=soulteary
          - PHABRICATOR_PASSWORD=soulteary
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.port=80"
          - "traefik.frontend.rule=Host:phabricator.soulteary.com"
          - "traefik.frontend.entryPoints=https,http"
    
    networks:
      traefik:
        external: true
    复制代码

    搭配 Traefik v2 使用

    当然,这里更推荐搭配 Traefik v2 一起使用。

    version: '3.7'
    
    services:
    
      phabricator:
        image: 'bitnami/phabricator:2019'
        expose:
          - 80
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.docker.network=traefik"
          - "traefik.http.routers.phab0.middlewares=https-redirect@file"
          - "traefik.http.routers.phab0.entrypoints=http"
          - "traefik.http.routers.phab0.rule=Host(`phabricator.lab.io`,`phabricator-file.lab.io`)"
          - "traefik.http.routers.phab1.middlewares=content-compress@file"
          - "traefik.http.routers.phab1.entrypoints=https"
          - "traefik.http.routers.phab1.tls=true"
          - "traefik.http.routers.phab1.rule=Host(`phabricator.lab.io`,`phabricator-file.lab.io`)"
          - "traefik.http.services.phabbackend.loadbalancer.server.scheme=http"
          - "traefik.http.services.phabbackend.loadbalancer.server.port=80"
        volumes:
          - './phabricator_data:/bitnami'
          - './extensions:/opt/bitnami/phabricator/src/extensions'
    
    networks:
      traefik:
        external: true
    复制代码

    对程序进行汉化

    感谢社区网友提供了程序的 汉化补丁,下载仓库中的 ** PhabricatorSimplifiedChineseTranslation.php** 并放置于上面配置文件指定的 extensions目录中后,启动应用,等待应用启动就绪后,在个人设置中切换中文即可。

    接下来开始正餐,如何基于源代码对 phabricator 进行容器封装。

    封装 Phabricator 容器镜像

    官方安装文档写的比较详细,甚至封装了一个基础的安装脚本,我们基于此进行容器的 Dockerfile 的编写。

    FROM ubuntu:18.04
    
    LABEL maintainer="soulteary@gmail.com"
    
    ENV DEBIAN_FRONTEND noninteractive
    
    RUN apt-get update && apt-get upgrade -y && \
        echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && export DEBIAN_FRONTEND=noninteractive && \
        apt-get install -y lsb-release curl && \
        # deps from: https://raw.githubusercontent.com/phacility/phabricator/stable/scripts/install/install_ubuntu.sh
        apt-get install -y tzdata sudo && \
        ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
        apt-get install -y git apache2 libapache2-mod-php php php-mysql php-gd php-curl php-apcu php-cli php-json php-mbstring php-zip python python-pip && \
        a2enmod rewrite && \
        pip install Pygments
    
    WORKDIR /opt
    
    RUN git clone https://github.com/phacility/phabricator.git --branch=stable --depth=1 && \
        cd phabricator && \
        git checkout cc11dff7d317b5a1e82e24ab571fef9abb633a49
    
    RUN git clone https://github.com/phacility/arcanist.git --branch=stable --depth=1 && \
        cd arcanist && \
        git checkout 729100955129851a52588cdfd9b425197cf05815
    
    RUN git clone https://github.com/phacility/libphutil.git --branch=stable --depth=1 && \
        cd libphutil && \
        git checkout 034cf7cc39940b935e83923dbb1bacbcfe645a85
    
    RUN git clone https://github.com/arielyang/phabricator_zh_Hans.git --branch=master --depth=1 && \
        cd phabricator_zh_Hans && \
        git checkout ba5e602d934a6efacdc09082cd3a762449de45cf && \
        cp dist/\(stable\)\ Promote\ 2019\ Week\ 50/PhabricatorSimplifiedChineseTranslation.php ../phabricator/src/extensions/
    
    COPY phabricator/docker-assets ./assets
    
    RUN sed -i -e "s/post_max_size = 8M/post_max_size = 32M/g" /etc/php/7.2/apache2/php.ini && \
        sed -i -e "s/;opcache.validate_timestamps=1/opcache.validate_timestamps = 0/g" /etc/php/7.2/apache2/php.ini
    
    RUN useradd daemon-user && \
        mkdir -p /data/repo && \
        chown -R daemon-user:daemon-user /data/repo && \
        ln -s /usr/lib/git-core/git-http-backend /usr/bin/ && \
        echo "Cmnd_Alias GIT_CMDS = /usr/bin/git*" >> /etc/sudoers.d/www-user-git && \
        echo "www-data ALL=(daemon-user) SETENV: NOPASSWD: GIT_CMDS" >> /etc/sudoers.d/www-user-git && \
        chmod 0440 /etc/sudoers.d/www-user-git
    
    ENTRYPOINT ["bash", "-c", "/opt/assets/entrypoint.sh"]
    
    EXPOSE 80
    复制代码

    Dockerfile 主要分为三个部分,第一部分进行基础系统环境配置、系统环境依赖;第二部分获取当前这个版本的程序代码和应用依赖;第三部分配置应用权限、设置容器启动脚本。

    这里所需的程序启动脚本 entrypoint.sh内容如下:

    #!/usr/bin/env bash
    
    ./phabricator/bin/storage upgrade --force && \
    ./phabricator/bin/phd start
    
    apachectl -D FOREGROUND
    复制代码

    执行的工作也很简单:初始化Phabricator 配置,并启动 Web Server。

    相关代码我已经上传至 GitHub,并推送至 DockerHub有需求的同学可以自取。

    编写服务配置

    服务配置分为两部分,第一部分是 Web Server 使用的。

    
      ServerName phabricator.lab.io
      DocumentRoot /opt/phabricator/webroot
      RewriteEngine on
      RewriteRule ^(.*)$/index.php?__path__=$1 [B,L,QSA]
    
      SetEnv HTTPS true
    
      "/opt/phabricator/webroot">
        Require all granted
    
    
    
    
      ServerName phabricator-file.lab.io
      DocumentRoot /opt/phabricator/webroot
      RewriteEngine on
      RewriteRule ^(.*)$/index.php?__path__=$1 [B,L,QSA]
    
      "/opt/phabricator/webroot">
        Require all granted
    
    
    复制代码

    将上面内容中的域名替换为自己实际使用的地址后,保存为 ** phabricator.conf**,接着准备应用配置:

    {"phabricator.base-uri": "https://phabricator.lab.io/","security.alternate-file-domain":"https://phabricator-file.lab.io/","pygments.enabled": true,"phabricator.timezone":"Asia/Shanghai","storage.local-disk.path":"/data/stor","repository.default-local-path": "/data/repo","phd.user": "daemon-user","mysql.pass": "phabricator","mysql.user": "root","mysql.port": "3306","mysql.host": "mariadb"
    }
    复制代码

    同样,替换域名为你自己的,并且将配置中的数据库相关内容替换为实际的数值,将文件保存为 local.json。(如果数据库使用的是本文的内容,可以不需要修改)

    编写容器启动配置

    将上面保存的配置文件放置到指定目录后,编写应用启动使用的 docker-compose.yml

    version: '3.7'
    
    services:
    
      phabricator:
        image: soulteary/phabricator:stable-2020-week-5
        expose:
          - 80
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.docker.network=traefik"
          - "traefik.http.routers.phab0.middlewares=https-redirect@file"
          - "traefik.http.routers.phab0.entrypoints=http"
          - "traefik.http.routers.phab0.rule=Host(`phabricator.lab.io`,`phabricator-file.lab.io`)"
          - "traefik.http.routers.phab1.middlewares=content-compress@file"
          - "traefik.http.routers.phab1.entrypoints=https"
          - "traefik.http.routers.phab1.tls=true"
          - "traefik.http.routers.phab1.rule=Host(`phabricator.lab.io`,`phabricator-file.lab.io`)"
          - "traefik.http.services.phabbackend.loadbalancer.server.scheme=http"
          - "traefik.http.services.phabbackend.loadbalancer.server.port=80"
        volumes:
          - ./phabricator_data/stor:/data/stor
          - ./phabricator_data/repo:/data/repo
          - ./phabricator/docker-assets/phabricator.conf:/etc/apache2/sites-available/000-default.conf:ro
          - ./phabricator/docker-assets/local.json:/opt/phabricator/conf/local/local.json:ro
    
    networks:
      traefik:
        external: true
    复制代码

    使用 docker-compose up -d将应用启动,并执行 docker-compose logs -f查看应用启动状况。

    Creating phabricator-dockerize_phabricator_1 ... done
    Attaching to phabricator-dockerize_phabricator_1
    phabricator_1  | Loading quickstart template onto "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:db.paste" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190523.myisam.01.documentfield.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190718.paste.01.edge.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190718.paste.02.edgedata.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190718.paste.03.paste.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190718.paste.04.xaction.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190718.paste.05.comment.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190802.email.01.storage.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190802.email.02.xaction.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190815.account.01.carts.php" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190815.account.02.subscriptions.php" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190816.payment.01.xaction.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190816.subscription.01.xaction.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190822.merchant.01.view.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190909.herald.01.rebuild.php" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190924.diffusion.01.permanent.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20190924.diffusion.02.default.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20191028.uriindex.01.rebuild.php" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20191113.identity.01.email.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20191113.identity.02.populate.php" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20191113.identity.03.unassigned.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20191114.email.01.phid.sql" to host "mariadb:3306"...
    phabricator_1  | Applying patch "phabricator:20191114.email.02.populate.php" to host "mariadb:3306"...
    phabricator_1  | Storage is up to date. Use "storage status" for details.
    phabricator_1  | Synchronizing static tables...
    phabricator_1  | Verifying database schemata on "mariadb:3306"...
    phabricator_1  | 
    phabricator_1  | 
    phabricator_1  | Database                 Table                 Name         Issues
    phabricator_1  | phabricator_differential differential_revision phid         Surplus Key
    phabricator_1  | phabricator_differential differential_revision key_modified Missing Key
    phabricator_1  | phabricator_differential differential_revision key_phid     Missing Key
    phabricator_1  | phabricator_repository   repository_identity   key_email    Missing Key
    phabricator_1  | phabricator_phortune     phortune_accountemail key_account  Missing Key
    phabricator_1  | phabricator_phortune     phortune_accountemail key_address  Missing Key
    phabricator_1  | phabricator_phortune     phortune_accountemail key_phid     Missing Key
    phabricator_1  | phabricator_user         user_email            key_phid     Missing Key
    phabricator_1  | phabricator_conpherence  conpherence_index                  Better Table Engine Available
    phabricator_1  | Applying schema adjustments...
    phabricator_1  | Completed applying all schema adjustments.
    phabricator_1  |  ANALYZE  Analyzing tables...
    phabricator_1  |  ANALYZED  Analyzed 535 table(s).
    phabricator_1  | Freeing active task leases...
    phabricator_1  | Freed 0 task lease(s).
    phabricator_1  | Starting daemons as daemon-user
    phabricator_1  | Launching daemons:
    phabricator_1  | (Logs will appear in "/var/tmp/phd/log/daemons.log".)
    phabricator_1  | 
    phabricator_1  |     (Pool: 1) PhabricatorRepositoryPullLocalDaemon
    phabricator_1  |     (Pool: 1) PhabricatorTriggerDaemon
    phabricator_1  |     (Pool: 1) PhabricatorFactDaemon
    phabricator_1  |     (Pool: 4) PhabricatorTaskmasterDaemon
    phabricator_1  | 
    phabricator_1  | Done.
    phabricator_1  | AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.20.0.4. Set the 'ServerName' directive globally to suppress this message
    复制代码

    当看到 Done.的时候,就可以打开浏览器对 Phabricator 进行进一步配置啦。

    设置 Phabricator 管理员账户

    打开浏览器,输入你配置的域名后,Phabricator 将跳转至 Dashboard。

    安装完毕的 Phabricator

    剩下的事情就是根据你自己的需求进行应用配置啦。

    最后

    Phabricator 的搭建只是第一步,与现有仓库集成、与CI 集成等内容留与后续再写吧。

    --EOF


    我现在有一个小小的折腾群,里面聚集了一些喜欢折腾的小伙伴。

    在不发广告的情况下,我们在里面会一起聊聊软件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。

    喜欢折腾的小伙伴欢迎扫码添加好友。(请注明来源和目的,否则不会通过审核)

    关于折腾群入群的那些事

    为什么在做微服务设计的时候需要DDD?

    $
    0
    0

    记得之前在规划和设计微服务架构的时候,张队长给了我一个至今依然记忆深刻的提示:『你的设计蓝图里为什么没有看到DDD的影子呢?』

    随着对充血模型的领域认知的加深,我越加感觉到DDD的重要性。但是DDD内容繁多,是不是要深入去了解呢,我觉得不必入坑太深,个人浅见,它最核心的一点就是针对贫血模型的不足而设计,把原先传统的贫血模型里的业务逻辑层拎出来,融入到Domain层,这样面对复杂业务的规模化变更,我们只需要专注于Domain即可。

    回到主题,我们要了解的是微服务和DDD到底有什么关系呢?

    因为在互联网时代,软件所面临的问题域比以往要复杂得多,这种复杂性来源于不断扩展的问题域自身,也来源于创新变化,以及这种规模性增长所带来的挑战。

    然而一个人一个团队,他对复杂的事物的认知是有极限的,面对这种复杂问题唯一的方法就是分而治之。分主要考虑的是如何去分;治意味着分出来的每一个部分要能够独立的运行,能够互相的协作,完成整体的目标,能够一来应对外部变化所带来的冲击。

    微服务的缺陷

    微服务架构在分和治两个方面都给出了很好的理论指导和最佳实践,那微服务是不是解决复杂问题的银弹呢?其实不然,很多团队在应用了微服务架构来构建他们的系统以后,发现并没有完全解决这种复杂性问题,甚至还带来了一些其他的问题。比如:
    • 服务并没有解决复杂系统如何应对需求变化这个问题,甚至还加剧了这个问题。
    • 当一个需求变化了,需要花大量的精力去识别这个变化影响到了哪些微服务,这些服务的多个团队之间,需要通过无休止的扯皮去决定哪个服务多一些,哪些服务少改一些。
    • 然后测试团队还需要做昂贵的这种联调测试
    • 即便如此呢,开发团队依然不放心,还要通过一系列的开关控制,小心翼翼的去做切流,去做灰度发布。


    从业务层面来看,微服务架构没有避免这种散弹式的修改。甚至反而加重了他,这是为什么呢?一个重要的原因是微服务架构在分的这个纬度考虑的并不全面。

    DDD功用

    当我们去做分的这种工作的时候,具体拆分详见我的另外一篇文章《 微服务的拆分姿势》,需要考虑哪些维度呢?我觉得我们至少要考虑三个维度:
    • 功能纬度
    • 质量纬度,比如性能,可用性
    • 工程纬度


    微服务对第2个给出了很好的指导,对第3个也给出了一些建议。但是,对第1个功能纬度只给出来非常有限的指导,就是为什么随着微服务的流行,领域驱动设计(DDD)又被重新重视起来的原因。

    DDD弥补了微服务在功能划分方面没有给出很好指导的缺陷。所以他们在面对复杂问题和构建系统时候是一种互补的关系,在系统拆分的时候可以很好的协作。

    只是他们看待系统拆分这个角度是不同的。微服务当中的服务所关注的范围正是DDD所推崇的六边形架构中的领域层。
    1.png

    拆分案例

    接下来结合DDD和微服务来拆分一个复杂系统。

    关于领域

    我们称企业的业务范围和在这个范围里进行的活动为领域,和软件系统无关。领域会分成多个子域,比如我们一个电商系统,会有:
    • 商品子域
    • 订单子域
    • 库存子域等等。


    在不同的子域里,不同的概念有不同的含义。所以我们在进行领域建模的时候,必须要有一个明确的领域边界,也就是DDD里称做的限界上下文,它是系统内部的一个架构边界,决定了这个系统架构。

    划分系统内部架构边界

    架构简洁之道这本书里边就说过:『系统架构是由系统的内部架构边界以及边界之间的依赖关系所决定的,与系统中各个组件之间的通信和调用的方式是无关的』。我们常说的微服务的服务调用本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,系统架构无关。

    所以,复杂系统划分的第一重要的是要划分内部的架构边界,即划分清楚这个上下文,以及明确他们之间的关系,这对应于我们之前说的功能的维度。这正是DDD用武之处。其次我们才考虑基于非功能的维度如何划分,这是微服务能够发挥其优势的地方。

    举个例子,我们把系统分成ABC三个个上下文,三个上下文的代码可以在一个部署单元里运行,通过进程内调用来完成操作,这就是典型的单体架构;
    2.png

    也可以各自在一个独立的部署单元里运行,通过远程调用来完成操作,这就是现在流行的微服务架构。

    边界清晰的好处

    我们更多的是两种架构模式的一个混合,比如A和B一起是一个部署单元,C是另外一个独立的部署单元,这种情况往往是因为C非常重要,他并发的访问量非常大,或者它的需求变更比较频繁。将C拆分出来的有以下几个好处:
    • 资源倾斜
    • 使用弹力设计模式:比如重试,熔断,降级
    • 使用特殊技术:比如Go语言
    • 具备独立代码库:有独立团队和运维人员,和A和B的运行期做到隔离不互相影响


    这四点正是服务架构所关注的,它是基于非功能纬度的视角来看待拆分这件事情的,他关注的不是系统架构的逻辑边界,更多的关注的是应用程序行为的分隔。

    那为什么不把A和B都拆成一个独立的部署单元?

    这会带来更多的好处,也会带来额外的成本,架构应该是可以演进的,在业务发展的早期,应该关注系统架构的逻辑边界,保持逻辑边界的清晰和关系的正确,随着业务量的增加,逐步在做拆分,这是组合应用DDD和微服务架构带来的最大的好处。

    在单体架构中,保持架构逻辑边界不被突破是有一定难度。如果逻辑边界不清晰,在需要服务器拆分的时候,就未必能拆得出来了。另外没有人一下子就可以把逻辑边界定义正确,即使这个上下文定义的不太正确,在DDD聚合根这个概念可以保障我们能够演进出更适合的上下文。

    DDD界限上下文内部通过实体和值对象来对领域概念进行建模,一组实体和值子对象归属于一个聚合根。那按DDD要求:
    • 聚合根用来保证内部实体规则的正确性和数据的一致性
    • 外部对象只能通过ID来引用聚合根,不能引用聚合根内部的实体
    • 聚合根之间不能共享一个数据库事务,它们之间的数据一致性需要通过最终的一致性来保障


    有了聚合根,基于这些约束,未来可以根据需要把聚合根升级为上下文,甚至拆分成微服务都是比较容易的。

    另外想要知道如何合理的拆分微服务,可以参考我的另外一篇文章《 微服务划分的姿势》,今天就给你介绍到这儿,希望对你有所启发。

    原文链接: https://www.cnblogs.com/jackyfei/p/12089123.html

      kafka集群broker节点扩容方案_watermelonbig的专栏-CSDN博客

      $
      0
      0
      在用kafka集群有3个节点,即host1, host2, host3,现需要将broker节点扩容至6个,以提供更高的数据处理能力。
      一、上架物理服务器用于提供更多的资源
      新扩容3个broker节点,host4,host5,host6

      二、在三台新增节点上部署kafka应用程序
      这些不是本文的重点,略过。

      三、重新分布原有的topic分区

      1、查看集群中当前所有可用的topic
      ./kafka-topics.sh --list --zookeeper ip:port
      lcf-201612201649
      test-for-sys-monitor
      2、查看特定topic的详细信息
      ./kafka-topics.sh --describe --zookeeper 192.168.1.92:2181 --topic lcf-201612201649
      Topic:lcf-201612201649    PartitionCount:24    ReplicationFactor:3    Configs:
          Topic: lcf-201612201649    Partition: 0    Leader: 1    Replicas: 1,3,2    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 1    Leader: 2    Replicas: 2,1,3    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 2    Leader: 3    Replicas: 3,2,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 3    Leader: 1    Replicas: 1,2,3    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 4    Leader: 2    Replicas: 2,3,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 5    Leader: 3    Replicas: 3,1,2    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 6    Leader: 1    Replicas: 1,3,2    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 7    Leader: 2    Replicas: 2,1,3    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 8    Leader: 3    Replicas: 3,2,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 9    Leader: 1    Replicas: 1,2,3    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 10    Leader: 2    Replicas: 2,3,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 11    Leader: 3    Replicas: 3,1,2    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 12    Leader: 1    Replicas: 1,3,2    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 13    Leader: 2    Replicas: 2,1,3    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 14    Leader: 3    Replicas: 3,2,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 15    Leader: 1    Replicas: 1,2,3    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 16    Leader: 2    Replicas: 2,3,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 17    Leader: 3    Replicas: 3,1,2    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 18    Leader: 1    Replicas: 1,3,2    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 19    Leader: 2    Replicas: 2,1,3    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 20    Leader: 3    Replicas: 3,2,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 21    Leader: 1    Replicas: 1,2,3    Isr: 1,3,2
          Topic: lcf-201612201649    Partition: 22    Leader: 2    Replicas: 2,3,1    Isr: 3,1,2
          Topic: lcf-201612201649    Partition: 23    Leader: 3    Replicas: 3,1,2    Isr: 3,1,2

      3、kafka集群分区日志迁移
      我们的目标是把原来分布在3个节点上的topic(24partitions, 3replicas),将全部partitions重新分布到全部的6个节点上去。

      (1) 先制作topics-to-move.json文件
      cat << EOF > topic-to-move.json
      {"topics": [{"topic": "lcf-201612201649"}],
      "version":1
      }
      EOF

      (2)使用-generate生成迁移计划
      [testuser@c4 bin]$ ./bin/kafka-reassign-partitions.sh --zookeeper 192.168.1.92:2181 --topics-to-move-json-file  ./plans/topic-to-move.json  --broker-list "1,2,3,4,5,6" --generate
      Current partition replica assignment
      {"version":1,"partitions":[{"topic":"lcf-201612201649","partition":1,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":8,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":19,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":15,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":18,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":13,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":0,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":10,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":5,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":12,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":17,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":9,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":7,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":20,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":23,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":3,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":2,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":4,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":11,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":6,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":14,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":22,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":16,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":21,"replicas":[1,2,3]}]}
      Proposed partition reassignment configuration
      {"version":1,"partitions":[{"topic":"lcf-201612201649","partition":1,"replicas":[4,1,2]},{"topic":"lcf-201612201649","partition":15,"replicas":[6,5,1]},{"topic":"lcf-201612201649","partition":8,"replicas":[5,3,4]},{"topic":"lcf-201612201649","partition":19,"replicas":[4,5,6]},{"topic":"lcf-201612201649","partition":13,"replicas":[4,3,5]},{"topic":"lcf-201612201649","partition":18,"replicas":[3,4,5]},{"topic":"lcf-201612201649","partition":0,"replicas":[3,6,1]},{"topic":"lcf-201612201649","partition":10,"replicas":[1,5,6]},{"topic":"lcf-201612201649","partition":5,"replicas":[2,5,6]},{"topic":"lcf-201612201649","partition":12,"replicas":[3,2,4]},{"topic":"lcf-201612201649","partition":9,"replicas":[6,4,5]},{"topic":"lcf-201612201649","partition":17,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":7,"replicas":[4,2,3]},{"topic":"lcf-201612201649","partition":20,"replicas":[5,6,1]},{"topic":"lcf-201612201649","partition":23,"replicas":[2,3,4]},{"topic":"lcf-201612201649","partition":3,"replicas":[6,3,4]},{"topic":"lcf-201612201649","partition":4,"replicas":[1,4,5]},{"topic":"lcf-201612201649","partition":2,"replicas":[5,2,3]},{"topic":"lcf-201612201649","partition":11,"replicas":[2,6,1]},{"topic":"lcf-201612201649","partition":6,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":22,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":14,"replicas":[5,4,6]},{"topic":"lcf-201612201649","partition":16,"replicas":[1,6,2]},{"topic":"lcf-201612201649","partition":21,"replicas":[6,1,2]}]}
      [testuser@c4 bin]$
      注:生成一个向broker 1,2,3,4,5,6迁移指定topic的迁移计划。输出内容中包括当前的分布配置和即将改变后的分布配置。
      将以上命令的输出内容保存为json文件。其中当前分布配置备份为backup.json,改变后的分布配置保存为expand-cluster-reassignment.json

      (3)使用-execute执行迁移计划
      [testuser@c4 kafka]$ ./bin/kafka-reassign-partitions.sh --zookeeper 192.168.1.92:2181 --reassignment-json-file ./plans/reassignment-lcf-201612201649.json --execute
      Current partition replica assignment
      {"version":1,"partitions":[{"topic":"lcf-201612201649","partition":1,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":8,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":19,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":15,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":18,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":13,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":0,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":10,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":5,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":12,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":17,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":9,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":7,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":20,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":23,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":3,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":2,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":4,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":11,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":6,"replicas":[1,3,2]},{"topic":"lcf-201612201649","partition":14,"replicas":[3,2,1]},{"topic":"lcf-201612201649","partition":22,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":16,"replicas":[2,3,1]},{"topic":"lcf-201612201649","partition":21,"replicas":[1,2,3]}]}
       
      Save this to use as the --reassignment-json-file option during rollback
      Successfully started reassignment of partitions {"version":1,"partitions":[{"topic":"lcf-201612201649","partition":12,"replicas":[3,2,4]},{"topic":"lcf-201612201649","partition":17,"replicas":[2,1,3]},{"topic":"lcf-201612201649","partition":8,"replicas":[5,3,4]},{"topic":"lcf-201612201649","partition":7,"replicas":[4,2,3]},{"topic":"lcf-201612201649","partition":23,"replicas":[2,3,4]},{"topic":"lcf-201612201649","partition":4,"replicas":[1,4,5]},{"topic":"lcf-201612201649","partition":19,"replicas":[4,5,6]},{"topic":"lcf-201612201649","partition":2,"replicas":[5,2,3]},{"topic":"lcf-201612201649","partition":20,"replicas":[5,6,1]},{"topic":"lcf-201612201649","partition":11,"replicas":[2,6,1]},{"topic":"lcf-201612201649","partition":16,"replicas":[1,6,2]},{"topic":"lcf-201612201649","partition":5,"replicas":[2,5,6]},{"topic":"lcf-201612201649","partition":14,"replicas":[5,4,6]},{"topic":"lcf-201612201649","partition":10,"replicas":[1,5,6]},{"topic":"lcf-201612201649","partition":18,"replicas":[3,4,5]},{"topic":"lcf-201612201649","partition":22,"replicas":[1,2,3]},{"topic":"lcf-201612201649","partition":15,"replicas":[6,5,1]},{"topic":"lcf-201612201649","partition":1,"replicas":[4,1,2]},{"topic":"lcf-201612201649","partition":6,"replicas":[3,1,2]},{"topic":"lcf-201612201649","partition":21,"replicas":[6,1,2]},{"topic":"lcf-201612201649","partition":13,"replicas":[4,3,5]},{"topic":"lcf-201612201649","partition":0,"replicas":[3,6,1]},{"topic":"lcf-201612201649","partition":9,"replicas":[6,4,5]},{"topic":"lcf-201612201649","partition":3,"replicas":[6,3,4]}]}

      (4)使用--verify进行迁移结果的验证
      [testuser@c4 kafka]$ ./bin/kafka-reassign-partitions.sh --zookeeper 192.168.1.92:2181 --reassignment-json-file ./plans/reassignment-lcf-201612201649.json --verify

      查看重分布结果:
      ./bin/kafka-topics.sh --describe --zookeeper 192.168.1.92:2181 --topic lcf-201612201649

      当以上迁移过程导致kafka的leader分布,不符preferred replica分布建议,则可以手动进行再平衡维护。
      注:进行分区迁移时,最好先保留一个分区在原来的磁盘,这样不会影响正常的消费和生产。部分迁移则支持正常消费和生产。

      (5)关于kafka集群leader平衡机制的维护
      ./kafka-preferred-replica-election.sh --zookeeper ip:port
      或者在配置文件中将参数设置为:
      auto.leader.rebalance.enable=true


      关于Kafka broker IO的讨论 - huxihx - 博客园

      $
      0
      0

        Apache Kafka是大量使用磁盘和页缓存(page cache)的,特别是对page cache的应用被视为是Kafka实现高吞吐量的重要因素之一。实际场景中用户调整page cache的手段并不太多,更多的还是通过管理好broker端的IO来间接影响page cache从而实现高吞吐量。我们今天就来讨论一下broker端的各种IO操作。

        开始之前,还是简单介绍一下page cache:page cache是内核使用的最主要的磁盘缓存(disk cache)之一——实际上Linux中还有其他类型的磁盘缓存,如dentry cache、inode cache等。通常情况下Linux内核在读写磁盘时都会访问page cache。当用户进程打算读取磁盘上文件的数据时,内核会首先查看待读取数据所在的page是否在page cache中,如果存在自然命中page cache,直接返回数据即可,避免了物理磁盘读操作;反之内核会向page cache添加一个新的page并发起物理磁盘读操作将数据从磁盘读取到新加page中,之后再返回给用户进程。Linux内核总是会将系统中所有的空闲内存全部当做page cache来用,而page cache中的所有page数据将一直保存在page cache中直到内核根据特定的算法替换掉它们中的某些page——一个比较朴素的算法就是LRU。同样地,在写入page数据到磁盘之前,内核也会检查对应的page是否在page cache中,如果不存在则添加新page并将待写入数据填充到该page中,此时真正的磁盘写还尚未开始,通常都是隔几秒之后才真正写入到底层块设备上——即这是一个延迟写入操作。理论上来说,在这几秒之内的间隔中,用户进程甚至还允许修改这些待写入的数据——当然对于Kafka而言,它的写入操作本质上是append-only的,故没有这样的使用场景。

        针对Kafka而言,我平时看到对page cache的调优主要集中在下面这3种上:

      • 设置合理(主要是偏小)的Java Heap size:很多文章都提到了这种调优方法。正常情况下(如没有downConvert的情形)Kafka对于JVM堆的需求并不是特别大。设置过大的堆完全是一种浪费甚至是“拖后腿”。业界对该值的设定有比较一致的共识,即6~10GB大小的JVM堆是一个比较合理的数值。鉴于目前服务器的硬件配置都非常好,内存动辄都是32GB甚至是64、128GB的,这样的JVM设置可以为内核预留出一个非常大的page cache空间。这对于改善broker端的IO性能非常有帮助
      • 调节内核的文件预取(prefetch):文件预取是指将数据从磁盘读取到page cache中,防止出现缺页中断(page fault)而阻塞。调节预取的命令是blockdev --setra XXX
      • 设置“脏页”落盘频率(vm.dirty_ratio):主要控制"脏页“被冲刷(flush)到磁盘的频率——当然还有个dirty_background_ratio,大家可以google它们的区别。在我看来,前者类似是一个hard limit,而后者更像个soft limit

        除了上面这几种,我更想讨论一下broker端自己的使用场景会对page cache造成什么影响进而反过来影响broker性能。目前broker端的IO主要集中在以下几种:

      • Producer发送的PRODUCE请求
      • ISR副本/非滞后consumer发送的FETCH请求
      • 滞后consumer发送的FECTH请求
      • 老版本consumer发送的FETCH请求
      • Broker端的log compaction操作

      一、Producer发送的PRODUCE请求

        Producer发送消息给broker,broker端写入到底层物理磁盘,这是Kafka broker端最主要的磁盘写操作了。真正的写入操作是异步的,就像之前说的,broker只是将数据直接写入到page cache中。何时写回到磁盘由操作系统决定,Kafka不关心。显然,当prodcuer持续发送数据时,page cache中会不断缓存当前发送的消息序列。这些数据何时会被访问呢?有三个可能的时机:

      • ISR副本拉取:当leader broker成功地写入了一条消息后,follower broker会从leader处拉取该条消息,如果是ISR的follower副本,通常能够很快速地拉取这条新写入消息,那么此时这条消息依然保存在leader broker页缓存的概率就会很大,可以保证直接命中。再结合sendfile系统调用提供的Zero Copy特性内核就能直接将该数据从page cache中输送到Socket buffer上从而快速地发送给follower,避免不必要的数据拷贝
      • Broker端compaction:当写入消息成功一段时间后log cleaner可能会立即开启工作,故compaction也有可能会触碰到这条消息。当然这种几率比较小,因为log cleaner不会对active日志段进行操作,而写入的消息有较大几率依然保存在active日志段上
      • Consumer读取:对于非滞后consumer(nonlagging consumer)而言,它们会立即读取到这条消息。和ISR副本拉取情况相同,这些consumer的性能也会比较好,因为可以直接命中page cache

      二、ISR副本/非滞后consumer发送的FETCH请求

        Broker端最重要的读操作! ISR副本和非滞后consumer都几乎是“实时”地读取page cache中的数据,而不需要发起缓慢的物理磁盘读操作。再加上上面说的Zero Copy技术既实现了快速的数据读取,也避免了对磁盘的访问,从而将磁盘资源保存下来用于写入操作。由此可见,这是最理想的情况,在实际使用过程中我们应该尽量让所有consumer都变成non-lagging的。对于这种Broker IO模式而言,此时的Kafka已经有点类似于Redis了。

      三、滞后consumer发送的FECTH请求

        真实场景下这种consumer一定存在的,不管是从头开始读取的consumer还是长时间追不上producer进度的consumer,它们都属于这类消费者。它们要读取的数据有大概率是不在page cache中的,所以broker端所在机器的内核必然要首先从磁盘读取数据加载到page cache中之后才能将结果返还给consumer。这还不是最重要的,最重要的是这种consumer的存在“污染”了当前broker所在机器的page cache,而且本来可以服务于写操作的磁盘现在要读取数据了。平时应用中,我们经常发现我们的consumer无缘无故地性能变差了,除了查找自己应用的问题之外,有时候诊断一下有没有lagging consumer间接“捣乱”也是必要的。

      四、老版本consumer发送的FETCH请求

        老版本consumer识别的数据格式与broker端不同,因此和走Zero Copy路径的consumer不同的是,此时broker不能直接将数据(可能命中page cache也可能从磁盘中读取)直接返回给consumer,而是必须要先进行数据格式转换,即所谓的downConvert——一旦需要执行downConvert,此broker就失去了Zero Copy的好处,因为broker需要将数据从磁盘或page cache拷贝到JVM的堆上进行处理。显然这势必推高堆占用率从而间接地减少了page cache的可用空间。更糟的是,当broker JVM线程处理完downConvert之后,还需要把处理后的数据拷贝到内核空间(不是page cache。因为失去了Zero Copy,所以必须先拷贝到内核空间然后才能发送给Socket buffer),再一次地压缩了page cache的空间。如果数据量很大,那么这种场景极易造成JVM堆溢出(OOM)。值得一提的是, KIP-283解决了容易出现OOM的问题,但依然不能使得downConvert场景继续“享受”Zero Copy。

      五、Broker端的log compaction操作

        Compaction操作定期处理日志段上的数据,执行基于key的压实操作。在compact期间,broker需要读取整个日志段,在JVM堆上构建映射表,因此也会挤占page cache的空间,另外compact会将处理结果写回到日志段中。Compaction是定时运行的操作,在频率上并不如上面4个来的频繁。

       

        综合比较上面这5种IO,我们希望broker端的磁盘尽量为producer写入服务,而page cache尽量为non-lagging consumer服务——这应该是能获取clients端最大吞吐量的必要条件。但在实际应用中,我们的确也观测到了因为lagging consumer或downConvert甚至是compaction导致其他clients被影响的实例。究其原因就是因为所有IO对page cache的影响是无差别的。producer持续写入保证了page cache中不断充满最新的数据,但只有存在一个auto.offset.reset=earliest的consumer,就有可能瞬间把page cache修改得面目全非,即使这个consumer只是一个一次性的测试consumer。从根本上来说,我们应该区分不同clients或进程对page cache访问的优先级。

        实际上,Linux的open系统调用提供了O_DIRECT的方式来禁用page cache,因此我在想能否为Kafka clients提供这样的选择,即可以指定某些clients或Kafka线程以O_DIRECT方式来访问Linux的VFS。如果支持这个功能的话,那么像这种一次性的auto.offset.reset=earliest的consumer抑或是阶段性的compact完全可以采用这种方式从而完全避免对page cache的“污染”。不过令人遗憾的是,Java直到Java 10才加入了对O_DIRECT的支持(https://bugs.openjdk.java.net/browse/JDK-8164900)。也许在未来Kafka不再支持Java 9时这会是一个不错的KIP提议吧。

      疫情之下,远程办公的10大指南

      $
      0
      0

      编者按:本文来自微信公众号 “人力资源分享汇”(ID:hrgogogo),作者 HRGO,36氪经授权发布。

      疫情还在继续。

      从今天开始,大部分人就已经开始为期一周,甚至更长的远程办公生涯。

      这一周的在家远程办公,对国家是至关重要的,因为自我隔离,可以有效防止疫情的传播,我们每个人其实都在为战胜疫情而做出自己的贡献。

      但远程办公,并没有那么容易,它最大的挑战就是人和人无法面对面的交流,管理效率和沟通效率会大幅下降,同时员工的工作状态不可控、工作进度不可控,团队的协同不可控,团队的氛围不可控... ...

      诸多挑战下,要做好远程办公,这10点最关键。

      01 每日明确固定沟通机制

      公司内部可以这样规定:

      ①从今天开始,每天早晨9点,开启远程早会机制。

      ②每天下午6点,开启晚会机制,把每天的的工作进度进行规整。

      ③每天晚上12点前,相关责任人要将工作进度表发到邮箱/钉钉中。

      不管是早会还是晚会,内容建议在公司范围内全部公开,尽量让所有人都周知每位成员的在这一周远程办公的工作内容,方便内部协作。

      以此可以让所有人都知道,虽然不在办公室上班,工作地点变了,但是你的工作的时间和工作的内容,都没有变。

      02 每日明确任务的分配

      在远程办公的过程中,因为人和人无法面对面沟通,导致沟通中信息量大量缺失,所以对任务指令的精准性提出更高的要求。

      所以要求分配任务时:

      1、明确目的、任务描述、完成时限、成员构成和明确的责任人;

      2、责任人要根据完成时限,合理地评估在什么时间内可以完成;

      3、责任人只要抓执行结果,跟进任务计划中的问题,不要在过程中查收;

      而任务一旦接受开始执行后,围绕任务执行进度、问题和计划的沟通都可以在微信群里逐步展开。

      03 每日明确任务响应时间

      远程办公的时候,经常会让人感觉很无力,因为看不到对方的状态,同事之间对响应时间的期待变得不确定。

      这会让团队之间的协同出现很大的问题,所以必须明确任务的响应的时间,譬如:

      ①随时看工作群消息,@你的消息,10分钟内回复

      ②所有任务都需要设定明确的Deadline

      ③所有指令明确时间,比如:《招聘需求表》请下午5点前提交

      04 提前准备远程办公工具箱

      HRGO整理了一套《人力资源从业者远程办公常用工具箱》,内容非常多,你可以点击图片进行查看,文末也有思维导图的PDF文件下载方式。

      05 正常作息,穿戴整齐,保持日常仪式感

      对于个人而言,要保证高效的远程工作,仪式感还是少不了。

      所以你要保证每天早晨起来后工作,要和上班时间时候的作息是一样的。换上正式一些的衣服,离开你的卧室,出来书房或者客厅办公,保持日常的仪式感。

      最好要保证你的桌面整洁有序,这样效率也会提高。

      (上图:HRGO年卡用户小杨同学的书桌)

      06 利用好每一个早晨,可参考「罐头工作法」

      要保证远程办公的高效,那么早晨就千万不能接触杂乱信息:知乎、微博、豆瓣、抖音、快手、今日头条....都不要看。

      接受杂乱无章的碎片信息会让你头脑混乱,毁掉早晨的状态,也会耽误你一整天的状态。

      在家里,没有人监督和敦促你,你就更需要快速地进入工作状态,你必须立即投入到紧张、有序、逻辑化、体系化的工作中。

      村上春树说,自己工作的时候,一般就是独自闷在屋里,有时状态连贯甚至不吃午饭,效率奇高,它把这种工作模式叫:罐头工作法。

      07 让自己进入心流状态

      早上的工作,先处理自己擅长的事情。

      因为当你处理自己擅长事情的时候,你会将个人精神力完全投注在某种活动上,同时感到兴奋及充实感,这就是进入了心流状态。

      只要当天你的工作进入了心流状态,那么你今天可以解决的问题就会很多,远超平常。

      想要进入心流状态,那么就一定要有工具利器、固定且规律的时间、专属的位置、不受打扰的环境,这样的远程办公,你值得拥有。

      08 注意让自己即时记录

      在家办公的环境下,很少有人、有场景、有变化地去提醒你。

      那么你就要将听到任何重要信息、被分配到任何事务,第一时间快速记下要点。等不忙的时候,再拿出来这些笔记,进行补充、拓展、延伸、完善、思考。

      甚至,你可以发展出自己的一套速记法,用记号表达特定含义。比如,用叹号表示优先级,用粗线表示截止时间,用箭头表示对接人,用三角表示重点等等。

      09 记得要做 Checklist

      在每个远程办公结束的晚上,做一下你当天的 Checklist。

      任务指令、布置安排、项目推进、人员配备,都需要一条条予以确认。尤其是领导或者同事没有把需求或指令说清楚的项目,都要写在Checklist里面,跟对方确认。

      从理论上来说,只要Checklist做得足够严密,执行时严格确认每一条,任务的完成就不会出问题。

      这是在不可控的环境中,找到可控地解决问题的最佳办法。

      10 找你的同事,打卡互相监督

      现在很流行打卡社群。

      那不如和你的同事,一起组建一个团队内部,远程办公的打卡社群。

      每天按时上线工作、每天能按时吃饭运动,每天能按时休息学习,如果漏掉打卡就得发红包。

      通过这种互相监督,让团队所有人都自律起来。HRGO就是通过「你打卡我退费」,让上千位HR保持学习习惯,并同时提升自己的能力,已经超过5000位HR一起抱团学习。

      最后,希望能帮到在家办公的你。

      新官上任后:谷歌史上首次公布YouTube和云计算单独收入

      $
      0
      0

      对于谷歌母公司Alphabet来说,除了网页搜索、安卓操作系统之外,网络视频YouTube以及云计算也是其他两个极为重要的业务,过去谷歌从未披露过这两个业务的营收。据外媒最新消息,作为史上第一次,谷歌周一在其2019年第四季度收益报告中公布了其YouTube和云计算业务的经营数据。

      据国外媒体报道,2019年,YouTube通过视频广告创造了151.5亿美元的收入,仅第四季度就有47.2亿美元。该部门在2018年创造了111.6亿美元的收入,包括当年第四季度的36.1亿美元。对比可以发现,YouTube去年的广告收入同比增长了三分之一,虽然是一项非常老牌的网络视频服务,但是增速仍然保持了高位。

      需要指出的是,这一部分收入不包括YouTube的非广告收入,比如“YouTube电视”(互联网电视频道直播服务)的会员费,这些收入囊括在了谷歌的其他收入部分。

      谷歌的云计算业务在2019年创造了89.2亿美元的收入,第四季度创造了26.1亿美元。相比之下,2018财年云计算业务的收入为58.4亿美元,当年第四季度为17.1亿美元。两相比较,谷歌云计算业务去年同比增长了近一半。

      云计算是谷歌高管并不满意的业务,市场研究公司的报告显示,谷歌云计算的份额仅为个位数,甚至仅相当于行业领先者亚马逊和微软公司的几分之一。

      据报道,谷歌高管过去曾经开会,掌门人佩奇差一点决定放弃云计算业务,但是最终决定扩大投资,希望能够在这一市场进入前两名,这意味着至少要超过排名第二位的微软公司。

      在公布四季度财报后,谷歌母公司Alphabet的股价下跌约3.5%。

      分析师们长期以来一直呼吁谷歌单独报告YouTube的收益,因为它被认为是其网络广告业务的重要组成部分。

      如果把YouTube当做一个独立的影视媒体公司,2018年,该公司从广告中获得的收入超过了探索公司(探索频道母公司),后者的年收入为106亿美元,而YouTube为112亿美元。探索公司尚未报告其2019年全年的结果。

      谷歌的云计算业务是该公司中一个年轻但快速增长的部分,正在拼命追赶微软和亚马逊。今年7月,谷歌表示,其云计算部门的年收入刚刚达到80亿美元,并计划在未来几年内将其销售额增加两倍。

      Alphabet第一次披露YouTube和云计算的业务,或许和该公司最近的高层人事变动有关系。

      众所周知的是,之前,谷歌首席执行官皮查伊兼任Alphabet首席执行官,过去的首席执行官佩奇和总裁布林辞去职位,退出了日常管理。皮查伊执掌的谷歌和Alphabet正在呈现出更大的透明度。

      另外,包括谷歌在内的美国四大科技巨头(谷歌、苹果、Facebook和亚马逊)正在接受美国政府多个部门的反垄断调查,谷歌业务信息的透明化,也有助于谷歌配合监管机构的调查,减少外界对于谷歌垄断市场的议论。

      据悉,一个由来自不同州和地区的50名司法部长组成的联盟之前宣布了他们对谷歌网络广告业务的调查。新的披露让投资者对调查人员可能正在关注的谷歌广告板块的不同部分有了更多的了解。

      在过去一年中,行业内掀起了网络视频大涨,更多的互联网公司、好莱坞和电视台正在效仿奈飞公司的模式,进入原创影视视频市场,这让奈飞、亚马逊等视频平台面临更大的竞争压力。

      不过,YouTube的商业模式则截然不同,YouTube是全世界最大的网民原创视频网站,视频制作质量不如奈飞的原创影视作品,但是内容规模庞大丰富,仍然占据了不菲的播放时间比例。

      根据早前美国派杰公司的一个调查,在美国青少年群体中,奈飞和YouTube各自占到了三分之一的视频播放时长,换言之,两个商业模式截然不同的视频巨头霸占了三分之二的时间份额。

      之前,YouTube也曾经“赶时髦”,进入了原创影视领域,投资拍摄美剧等作品,通过会员费获取收入。但是据外媒报道,YouTube在原创影视领域的战略并不成功,目前已经基本放弃,原先制作的一些美剧影视作品已经免费对大众开放,将通过视频广告回笼投资。

      文章

      Oracle的时区问题 - 永春 - 博客园

      $
      0
      0

      总结

      • DATETIMESTAMP类型不支持时区转换。
      • 如果应用和Oracle的时区不一致,那么应该使用 TIMESTAMP WITH LOCAL TIME ZONE
        • 对于JDBC程序来说,JVM时区和用户时区保持一致就行了。
      • 如果应用和Oracle的时区不一致,而且需要保存时区信息,那么应该使用 TIMESTAMP WITH TIME ZONE
      • 格式化日期时间字符串函数 TO_CHAR
        • 对于 TIMESTAMP WITH TIME ZONE来说,使用 TO_CHAR时要注意让它输出时区信息( TZH:TZM TZR TZD),否则结果会是截断的。
        • 对于 TIMESTAMP WITH LOCAL TIME ZONE来说,使用 TO_CHAR返回的结果会转换时区。
      • 当前日期时间的函数:
        • 除非必要,不要使用 SYSDATESYSTIMESTAMP,这个返回的是数据库所在操作系统的时间。
        • 尽量使用 CURRENT_TIMESTAMP,它返回的是 TIMESTAMP WITH TIME ZONE,能够用来安全的比较时间。
      Oracle 9i 开始多了 3 个关于时间的数据类型:TIMESTAMP [(precision)] TIMESTAMP [(precision)] WITH TIME ZONE TIMESTAMP [(precision)] WITH LOCAL TIME ZONE,其中 TIMESTAMP [(precision)] WITH TIME ZONE 保存了时区信息。

      1. Oracle 的时区设置


          Oracle 的时区可以分为两种,一种是数据库的时区,一种是 session 时区,也就是客户端连接时的时区(经过实验,连接以后再修改客户端的时区,session 的时区不会更改)。


          数据库的时区在创建数据库时可以通过在 create database 语句中加上 SET TIME_ZONE = ' { { + | - } hh : mi | time_zone_region } ' 来指定,如果,不指定,默认是按照数据库所在的操作系统时区来设定的。创建之后,可以通过 alter database 来修改。其中 time_zone_region 参数可以通过查询 V$TIMEZONE_NAMES 动态视图来获得所有支持的值。修改之后,需要重启数据库才能生效。经常有人会碰到无法修改的情况:


      SQL> alter database set time_zone='+06:00';
      alter database set time_zone='+06:00'
      *
      ERROR at line 1:
      ORA-02231: missing or invalid option to ALTER DATABASE

          TOM 对此问题有过 解释,TIME_ZONE 的设定主要是为了 WITH LOCAL TIME ZONE,当 session 的时区和数据库的时区不同时,oracle 根据时区的差距转换到数据库的时间,再保存到数据库的 WITH LOCAL TIME ZONE 类型中,他是不保存时区的,所以需要 TIME_ZONE 来进行各种时区之间时间的转换(WITH TIME ZONE 类型保存了原始的时区,所以不需要 TIME_ZONE 的设置也可以进行各种时区之间的转换)。但数据库中一旦有了该类型,就不能通过 alter database 修改时区了,会得到上面的错误,可以通过下面的语句获得所有包含该类型的表,将他们删除之后,再修改。


      select u.name || '.' || o.name || '.' || c.name TSLTZcolumn
        from sys.obj$ o, sys.col$ c, sys.user$ u
      where c.type# = 231
         and o.obj# = c.obj#
         and u.user# = o.owner#;
      (一般查询后的结果为:OE.ORDERS.ORDER_DATE,指的是OE用户下的ORDERS表的ORDER_DATE字段使用了时区的信息:WITH LOCAL TIME ZONE,将此信息去掉就可以再修改了,修改好了之后需要重启数据库才能生效)

          Session 的时区是根据客户端的时区来决定的,当然连接以后也可以通过 alter session 来改变。WITH LOCAL TIME ZONE 类型会根据 TIME_ZONE 的设置,自动把时间转换为 session 所在时区的时间显示出来,而 WITH TIME ZONE 因为保存了时区,不需要根据 TIME_ZONE  的设置来转换。


      2. 查看时区


          可以分别使用 SESSIONTIMEZONE / DBTIMEZONE 内建函数查看 session 和数据库时区:



      SYS@SKYDB> select dbtimezone from dual;


      DBTIME
      ------
      +08:00


      SYS@SKYDB> select sessiontimezone from dual;


      SESSIONTIMEZONE
      ---------------------------------------------
      +09:00



          另外可以用 TZ_OFFSET 查询某时区和 UTC 之间的差值。



      TZ_OFFSET ( { 'time_zone_name'
                              | '{ + | - } hh : mi'
                              | SESSIONTIMEZONE
                              | DBTMEZONE  }
                            )


      SELECT TZ_OFFSET('US/Eastern') FROM DUAL;


      TZ_OFFS
      -------
      -04:00

      SELECT TZ_OFFSET(DBTIMEZONE) FROM DUAL;

      TZ_OFFSET(DBTI
      --------------
      +08:00


          其中 time_zone_name 也可以从 V$TIMEZONE_NAMES 获得。


      3. 几个内建时间函数的比较


          sysdate/systimestamp 都是返回数据库的时间并且使用数据库的时区,他们返回的是操作系统的时间。sysdate 返回的是 date 类型,没有时区信息,操作系统上是什么时间就返回什么时间;systimestamp 返回 TIMESTAMP WITH TIME ZONE 类新,有时区信息:



      SYS@SKYDB> select sysdate from dual;


      SYSDATE
      -------------------
      2006-08-03 10:01:31

      SYS@SKYDB> select systimestamp from dual;


      SYSTIMESTAMP
      -----------------------------------------------
      03-AUG-06 10.02.21.093000 AM +08:00

      SYS@SKYDB> shutdown immediate
      Database closed.
      Database dismounted.
      ORACLE instance shut down.

      修改操作系统时区为 +02:00

      SYS@SKYDB> startup
      ORACLE instance started.


      Total System Global Area   89202456 bytes
      Fixed Size                   454424 bytes
      Variable Size              62914560 bytes
      Database Buffers           25165824 bytes
      Redo Buffers                 667648 bytes
      Database mounted.
      Database opened.

      SYS@SKYDB> select sysdate from dual;


      SYSDATE
      -------------------
      2006-08-03 04:03:37


      SYS@SKYDB> select systimestamp from dual;


      SYSTIMESTAMP
      ----------------------------------------------
      03-AUG-06 04.04.15.687000 AM +02:00

          注:这是我单位机子上实验的结果,由于建了多个数据库,不知道为什么不能通过 ipc 来连接本地数据了,登陆时使用 sqlplus " /@skydb as sysdba",也就是使用了监听器来连接,但在家里做相同的实验,通过 ipc 连接 sqlplus "/as sysdba",修改时区后,sysdate 依然显示修改前的时间,而 systimestamp 却正确,不知道是什么原因:

      SQL> select sysdate from dual;

      SYSDATE
      -------------------
      2006-02-08 22:21:40

      SQL> select systimestamp from dual;

      SYSTIMESTAMP
      ---------------------------------------------------------------------------
      02-AUG-06 10.22.38.578000 PM +08:00

      SQL> shutdown immediate
      Database closed.
      Database dismounted.
      ORACLE instance shut down.
      修改时区为 +09:00
      SQL> startup
      ORACLE instance started.

      Total System Global Area  131145064
      bytes

      Fixed Size                   453992
      bytes

      Variable Size             109051904
      bytes

      Database Buffers           20971520
      bytes

      Redo Buffers                 667648

      bytes

      Database mounted.
      Database opened.
      SQL> select sysdate from dual;

      SYSDATE
      ---------
      02-AUG-06

      SQL> alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss';

      Session altered.

      SQL> select sysdate from dual;

      SYSDATE
      -------------------
      2006-08-02 22:32:59              <- 还是之前的时间

      SQL> select systimestamp from dual;

      SYSTIMESTAMP
      ---------------------------------------------------------------------------
      02-AUG-06 11.35.05.171000 PM +09:00          <- 时间正确

          另外,有个初始化参数 fixed_date,可以设置 sysdate 返回指定的时间:



      alter system set fixed_date='2005-04-04-11-00-00'


      this fixed_date is normally used, in oracle, for dubugging purpose.


      once finishing it, you can set it back:


      alter system set fixed_date=none


          Eygle 的关于这个参数的相关文章: Why sysdate is fixed

          current_timestamp/current_date 也会返回数据库的时间,但转换为 session 的时区进行显示,可以使用 alter session set time_zone 改变 session 时区。


      4. 四个日期时间类型的实验


      SQL> select dbtimezone from dual;

      DBTIME
      ------
      +06:00

      SQL> select sessiontimezone from dual;

      SESSIONTIMEZONE
      ---------------------------------------------------------------------------
      +08:00

      SQL> ed
      Wrote file afiedt.buf

        1  create table tztest(a date,
        2  b timestamp(0),
        3  c timestamp(0) with time zone,
        4* d timestamp(0) with local time zone)
      SQL> /

      Table created.

      SQL> alter session set nls_date_format ='yyyy-dd-mm hh24:mi:ss';

      Session altered.

      SQL> select sysdate from dual;

      SYSDATE
      -------------------
      2006-02-08 22:21:40

      SQL> select systimestamp from dual;

      SYSTIMESTAMP
      ---------------------------------------------------------------------------
      02-AUG-06 10.22.38.578000 PM +08:00

      SQL> select current_date from dual;

      CURRENT_DATE
      -------------------
      2006-02-08 22:23:50

      SQL> select current_timestamp from dual;

      CURRENT_TIMESTAMP
      ---------------------------------------------------------------------------
      02-AUG-06 10.24.04.031000 PM +08:00

      SQL> insert into tztest
        2  values(sysdate,systimestamp,systimestamp,systimestamp);

      1 row created.

      SQL> commit;

      Commit complete.

      SQL> select * from tztest;

      A
      -------------------
      B
      ---------------------------------------------------------------------------
      C
      ---------------------------------------------------------------------------
      D
      ---------------------------------------------------------------------------
      2006-02-08 22:25:59
      02-AUG-06 10.25.59 PM
      02-AUG-06 10.25.59 PM +08:00
      02-AUG-06 10.25.59 PM

      SQL> exit
      Disconnected from Oracle9i Enterprise Edition Release 9.2.0.3.0 - Production
      With the Partitioning, OLAP and Oracle Data Mining options
      JServer Release 9.2.0.3.0 - Production

      修改了客户端操作系统的时区

      C:\Documents and Settings\Administrator>sqlplus sky/xxxx

      SQL*Plus: Release 9.2.0.3.0 - Production on Wed Aug 2 23:28:01 2006

      Copyright (c) 1982, 2002, Oracle Corporation.  All rights reserved.


      Connected to:
      Oracle9i Enterprise Edition Release 9.2.0.3.0 - Production
      With the Partitioning, OLAP and Oracle Data Mining options
      JServer Release 9.2.0.3.0 - Production

      SQL> alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss';

      Session altered.

      SQL> select sysdate from dual;

      SYSDATE
      -------------------
      2006-08-02 22:28:49        <-数据库没有重启,时间依然是修改前的

      SQL> select systimestamp from dual;

      SYSTIMESTAMP
      ---------------------------------------------------------------------------
      02-AUG-06 11.29.33.609000 PM +09:00  <- 这里却已经改变了,有时区信息,自动转换了?

      SQL> select * from tztest;

      A
      -------------------
      B
      ---------------------------------------------------------------------------
      C
      ---------------------------------------------------------------------------
      D
      ---------------------------------------------------------------------------
      2006-08-02 22:25:59                       <- 没变
      02-AUG-06 10.25.59 PM                  <- 没变
      02-AUG-06 10.25.59 PM +08:00      <- 保存时区信息
      02-AUG-06 11.25.59 PM                  <-自动转换为 session 的时区


      高阶增长:构建和应用3种增长模型

      $
      0
      0

      构建增长模型在我们的增长全景图中,处于找到增长发力点的下半部分,那么在构建模型之后在应用模型时也会帮助我们找到聚焦领域。

      No.1 什么是增长模型?

      增长模型就是找到所有对北极星指标有影响的所有细分指标,并用一个简单的公式,把这些细分指标联系起来的过程。其实就是从北极星指标到细分指标的拆解过程。

      ]

      为什么要构建增长模型?

      1. 比如:大家都会玩游戏,在玩游戏时对游戏中的人物有一个指令输入,经过游戏机或者电脑的处理 ,形成了输入导致游戏人物或者剧情有个结果。所以想要控制输入是通过控制输入而完成的。

      2. 比如:你想要减肥,改变体重是你的北极星指标。

      ]

      通过可以改变控制的输入变量 行为 去影响你的输出变量 结果 也就是北极星指标

      3. 比如:我们在做增长的时候同样的道理。

      ]

      你的北极星指标是日活跃用户,我们很难直接做一些什么来提升日活,我们能做的是影响这些的细分指标,app下载量、用户激活率、老用户留存。

      输入变量:单一因素、团队可直接操作、行为;

      输出变量:受多种因素影响,团队不可直接操作、结果。

      所以增长模型就是帮助大家把不可以操作的一个结果一个北极星指标,分解成可以操作的一些细分指标,然后大家可以通过细分指标改善最终的结果;

      No.2 如何构建增长模型?

      构建增长模型3步走:

      ]

      举例说明:对于网易云音乐是个听歌的应用,所以:

      • 第一步:北极星指标为总听歌时间
      • 第二步:绘制用户核心转化路径
      • 第三步:组装增长模型

      ]

      介绍两个比较常用的增长模型框架:

      ]

      1. 增长模型一应用:全链漏斗模型

      总听歌时间=应用下载量*首次访问%*首次听歌%*持续听歌%*平均听歌时长

      比如说每天下载量30000,每天有50%人首次打开我的app,在首次访问里的人有33%首次听歌,持续听歌人数5%(老用户听歌人数/老用户总数(天))平均听歌时长20分钟。

      全链漏斗模型:优点把影响北极星指标的主要细分指标都显示出来,并标注转化率,找到机会点,容易搭建。

      缺点:不能直接计算北极星指标。

      2. 增长模型二应用:因子分解模型

      ]

      • 因子分解模型:优点把影响北极星指标的主要细分指标都显示出来,并标注转化率,找到机会点,容易搭建。
      • 缺点:不能直接计算北极星指标。

      No.3 如何应用增长模型?

      1. 应用一:找到聚焦领域-四两拨千斤的增长发力点

      • 问题一:想要增长北极星指标,AARRR里可做的事情很多,如何决定先做什么后做什么?
      • 问题二:小公司资源有限,如何找到性价比最高的点将增长效果最大化?

      案例一:Airbnb通过增长模型找到发力点

      ]

      行业平均值这个指标如果不能提升到和行业一样的水平,那么想办法拉来很多流量其实没什么意义很多都会浪费掉,所以在现阶段这个点就是airbnb最重要的发力点。

      案例二:Sass软件用不同用户分群,分解增长模型,寻找机会。

      ]

      粗略的看起来好像发现除了使用购买率低一点,别的好像看不出来什么问题。这个时候比较有用的一个方法,我们按照一些重要的用户纬度,把增长模型进行分解。

      ]

      获客渠道最重要的两个部分是公号和广告,客户可能有大公司和小公司按人数来把增长模型进一步拆分出来,拆出之后可对比分析发现一些潜在的机会。

      比如说付费广告来源的用户试用注册率和购买率偏低但是订阅长度却很长,可能提示我们这个渠道并不是很多我们的精准用户要想办法提高精准程度,但通过这个渠道来源的用户成为付费用户后生命周期比较长,说明还可以优化这个渠道找到更感兴趣的用户不要盲目的追求流量。

      再比如小公司的分解纬度试用购买率很低同时付费用户订阅的长度也比较低,那可能我们的软件是不是不太适合小公司,可能是团队合作的软件对小公司来说访问量虽然很大但对于他们需求并不大,那么大公司可能是我们主要发力的群组。

      通过分群的模式分解增长模型对比不同的维度,他的增长模型的细分指标,也可以帮助我们找到改善北极星指标的机会。

      2. 应用二:寻找具体的增长思路

      • 问题一:业务很复杂,感觉千头万绪,不知道从何下手推动增长。
      • 问题二:想要改善北极星指标,但是落实到产品、运营等还是没有具体的想法如何做?

      案例:淘宝通过增长模型简化业务,寻找增长思路

      ]

      为了提升订单价或者让用户到达指定感兴趣的页面,增长模型很好的一点是把复杂的东西抽象化,反而比较容易理清思路。

      3. 应用三:指导指标拆分和团队协作

      • 问题一:作为增长负责人,定了北极星目标,如何把活分下去,并保证执行团队和我的思路一致减少失误?
      • 问题二:作为执行团队,如何清晰的知道自己的任务,可以在快节奏的日常工作中作出正确的决定?
      • 问题三:如果公司里有多个团队合作推动增长,如何保证大家劲往一处使?

      通过增长模型把北极星指标拆分为细分指标。

      常见的因子分解增长模型框架:

      • 横向分解:先按照用户群组,通过“加法”分解
      • 纵向分解:在按照用户漏斗,通过“乘法”分解

      ]

      注意⚠️:拆分细分指标关键

      遵循MECE原则:

      • ME:相互独立,每个细分指标完全独立(避免后续分配给团队时互相打架)
      • CE:完全穷尽,细分指标涵盖影响增长重要因素(避免增长中的盲点)

      协作模式一:每个跨功能团队单独承担一个细分指标

      ]

      协作模式二:多个团队合作推动同一细分指标

      ]

      细分指标还可以进一步拆分,以新用户量为例:

      目标:年度新用户数量50万

      新用户量=运营渠道获客团队(40万)+BD商务合作团队(5万)+产品推荐团队(5万:比如老带新功能等)

      运营渠道获客团队为负责新用户指标主要团队,BD和产品为支持团队。

      案例:网易的多团队如何协作推动增长

      情景:商业化部门将年收入目标,拆分到各个产品线,在单个产品线收入目标下,需要有好几个部门来协作如何推?

      第一次尝试:各部门背独立KPI,但出现各部门指标互相影响的情况下,比如广告部收入指标涨,但产品部用户活跃指标降,部门之间难以及时沟通,每个团队都是领域专家,但是汇集到一起反而降低产出。

      第二次尝试:成立用户增长中心,改成OKR模式,各部门首先有一个共同的北极星指标,同时给产品部、体验部拆分不同的细分指标,保证细分指标之间没有交集,团队不能考虑仅仅提升细分指标还要最终支持北极星的增长,避免了互相伤害的情况。

      总结:构建和应用三种增长模型

      ]

       

      本文由@叶子 原创发布于人人都是产品经理,未经许可,禁止转载

      题图来自Unsplash, 基于CC0协议

      曹德旺:我做好了和这个国家一起过苦日子的准备

      $
      0
      0

      曹德旺强调,当务之急是要集中精力先消灭疫情,等疫情结束,再一起探索解决问题的办法。“真真正正的救助是自救,企业首先要想办法自己救自己。”


      文3277字,阅读约需6分钟 

      一场突如其来的疫情,正在让许多中小企业经历着生存的历练与考验。 新年伊始,新冠肺炎疫情暴发,紧张的防控工作全面开启。疫情之外,中小企业的生存和发展也增加了新的挑战,特别是餐饮、酒店、旅游、实体零售业,其经营现状引起了社会广泛关注。 2月3日,习近平总书记主持召开中共中央政治局常务委员会会议,明确要求要切实维护正常经济社会秩序。在加强疫情防控的同时,努力保持生产生活平稳有序。 一场经济“战疫”已经打响。近日,新京报将与多位知名企业家进行深度交流,报道在当前疫情之下企业的真实生存现状以及如何自救与发展,共克时艰。今天我们推出“企业家访谈”第一期:曹德旺谈疫情冲击:企业家首先要自己想办法救自己。

       

      中小微企业的生存已到了千钧一发的时刻?

       

      近期,随着新型冠状病毒感染肺炎疫情这只“黑天鹅”的经济效应逐渐显现,企业、尤其是中小微企业的生产经营状况引发关注。至于受疫情影响有多大?福耀集团董事长曹德旺接受新京报记者专访时表示,服务业中的大中型企业、房地产业企业受疫情影响严重,小微企业受影响相对较小。

       

      谈及如何救助受影响的企业,他建议国家在一定时间段内免除企业五险一金费用以及税收等。在建议国家推出一定纾困措施的同时,曹德旺还强调,当务之急是要集中精力先消灭疫情,等疫情结束,再一起探索解决问题的办法。“真真正正的救助是自救,企业首先要想办法自己救自己。”

       


      ━━━━━

      酒旅、房地产受影响更大

       

      新京报:如何看待此次疫情对中小微企业的影响?

       

      曹德旺:在讨论这个问题前,我们首先要界定清楚什么是小微企业、什么是中型企业、什么叫大型企业?在我看来,人员在5人以上、50人以下的企业是小微企业,员工超过50人的企业是中型企业。

       

      小微企业以个体户为主,工作场地、吃住都在家里,成本和利润都不高。相对于大中型的服务业企业,总体看小微企业受疫情影响不严重(不是说小微企业不受影响),服务业中的大型酒楼、连锁酒店、旅游地产等大中型企业受疫情影响比较严重。此外,房地产行业受疫情影响更大。因为第一个季度本来是房地产销售的旺季,现在没人买房了,与此同时房企资金流问题一直突出,房企在没有收入的同时还要还银行的贷款,相信会有不少房地产企业会倒下。

       

      企业也必须真实反映情况,我们要警惕有些企业不怀好意,借助灾情标新立异、趁火打劫,吓唬政府。

       

      ▲2017年10月13日,曹德旺接受新京报记者专访。资料图片 彭子洋 摄


      新京报:制造业企业受疫情影响大吗?

       

      曹德旺:制造业本来就产能过剩,不用太担心疫情的影响。从汽车行业看,2019年汽车销量和产量都在下降,无论经销商、制造商都有大量的库存。在这种情况下,汽车生产线停个二十天一个月,不会大乱。

       

      新京报:福耀集团目前情况如何?

       

      曹德旺:福耀受影响很大。福耀集团每个月全球有几十亿的营业额,现在一半的工厂还不能开工,跟很多国际知名品牌汽车厂的订单都不能做了。以前每个月正常情况下,我们可以赚4个亿利润,现在不到2亿5000万元,我一句都没有吭声,还在给国家捐钱,帮国家买口罩。因为我知道当务之急是要集中精力先把疫情消灭,而不是把解决企业遇到的问题排在第一位。

       


      ━━━━━

      给小微企业免税 放水养鱼才是真帮助

       

      ▲曹德旺


      新京报:很多人在讨论如何救助中小微企业。在你看来,小微企业受疫情影响不大,不需要救助了?

       

      曹德旺:我不是不支持帮助小微企业,我曾经对民营企业做过系统的调查,小微企业一共面临三个问题:第一个问题是融资难问题。为什么银行不愿意给小微企业贷款?银行和企业一样,都是以营利为目的。银行给企业贷款,需要做尽调、风控等成本,但给小微企业贷款的利润并不能覆盖贷款成本,无钱可赚,银行自然不愿意给小微企业贷款了。小微企业融资难不是中国独有的现象,国外同样如此——无论是投行还是商业银行,都不会给小微企业发放贷款。第二个问题是小微企业经常被歧视。第三个是税收问题。

       

      比起解决融资的问题,帮助小微企业减税免税更重要。即使没有这次疫情,也早应该给小微企业免税,小微企业没有什么税可交。在国际上,其他国家的小微企业不用交增值税。小微企业是否还需要交企业所得税?一般的小微企业去掉房租、工资、借贷的高利息等成本,已经所剩并不多了。

       

      在我看来,不仅仅应该给小微企业免税,甚至应该取消小微企业的交税资格。放水养鱼,这个办法才是真正帮助小微企业。

       


      ━━━━━

      不建议强制增贷款,企业要想法自救

       

      新京报:疫情冲击之下,企业流动性等问题引发关注,你如何看待?

       

      曹德旺:企业出现问题可能会有各种各样的原因,并不一定是这次疫情带来的。确实有一些中型企业受疫情影响严重,流动性会出现问题。但我不建议强制银行给企业增加贷款,因为银行也是企业,必须按照商业原则做业务。

       

      对于受疫情影响的企业,国家能做什么呢?考虑到疫情会影响经济活动一两个季度,建议国家免除灾情期间企业至少半年的五险一金费用——因为在疫情期间,企业员工虽然不上班,但企业仍要支付一定的工资。而工资中五险一金占据了很大的比例,如果能够将这部分费用免除掉,可以极大减轻企业的压力,缓解受疫情冲击的程度。同时,还建议免除企业两三个月的税收帮企业减负。此外,还建议国家可以规定企业今年一年在计算成本时不将折旧费纳入其中。

       

      但救助也要分清主要和次要。我认为现在肺炎疫情当头,不是中小微企业出问题当头。当前最重要的事情是要集中全力想办法尽快消灭疫情,不要让其他的事情分散解决主要问题的精力。我坚决反对这个时候给国家提要求、施加压力,中国是中国人的中国,我们每个中国人都要有国家的概念,不要老想着自己的事情,都应该仗义承担疫情带来的影响,互相团结起来克服困难。等疫情结束了,我们再讨论如何恢复生产过日子,一起探索解决问题的办法。

       

      我也建议企业家自己想一想:在这次疫情中,大中小企业、工人、农民,谁不受影响,谁不困难?在中国14亿人中,有几千万或者一亿的精英当上了企业老板,而在这一亿精英身后,还有农民、工人、贫困人口、打工的人。现在的企业不要遇到一点问题就只想着依赖国家救助,更多财富、能力不如企业老板的人谁来救?真真正正的救助是自救,首先要想办法自己救自己。建议大家现在冷静一下,自立一点,想想企业最大的困难是什么,如何尽最大的努力去解决问题、渡过难关。

       


      ━━━━━

      疫情不会给中国经济带来太大影响

       

      新京报:在你看来,疫情对中国经济影响有多大?

       

      曹德旺:我不想讨论这个问题。现在疫情本身很严重了,这一个月经济活动停了,经济损失多大,还用讨论吗?但疫情只是影响了一些经济活动,GDP增速会后退一点,但疫情并没有完全影响中国经济,不会给中国经济带来太大影响,我们切忌过度紧张。

       

      紧张也解决不了问题,只会增加恐惧感。要重视疫情,但不要自己吓唬自己。

       

      新京报:疫情冲击会进一步加大中国经济下行的压力。

       

      曹德旺:中国经济、民企的很多问题并不是这次疫情造成的,很多问题一直存在,只是这次疫情让有些问题更突出。

       

      我们要弄清楚经济下行的主要原因是什么?部分是因为中国经济社会正在转型期,还有历史的原因——很多根本性的问题没有解决,越累积越严重。

       

      中国经济的发展得益于“改革开放”这四个字。各界都支持开放,打开国门引进外资、技术、管理,同时中国企业开始走出去。但改革的推进却很艰难,因为改革是权(力)和利(益)的重新分配。其中,国有经济体制的改革一直没有太大进展,国企改革应该怎么改?此外,我们也没有投资经验,只会拼命盖房子、修高速公路铁路。当然有的基建投资有必要,有的确实没有必要,我们公共工程动用的成本太大了,社会负债急剧上升。现在刹车还来得及,否则后果不堪设想。

       

      现在全球经济形势都不好,中国经济很多的问题还没有解决,我做好了和这个国家一起过苦日子的准备。

       

      新京报记者 侯润芳 实习生 赵方园


      点击下图进入" 全国新型冠状病毒感染肺炎实时地图"

      值班编辑 花木南 吾彦祖


      日本载3700人被隔离游轮曝光,船上乘客讲述“海上隔离”


      关注武汉,也别漏了那些“难上热搜”的城市


      质疑声中的湖北红会


      本文未经新京报书面授权不得转载使用

      欢迎朋友圈分享


      Elasticsearch 索引设计实战指南

      $
      0
      0

      题记

      随着 Elastic 的上市,ELK Stack 不仅在 BAT 的大公司得到长足的发展,而且在各个中小公司都得到非常广泛的应用,甚至连“婚庆网站”都开始使用 Elasticsearch 了。随之而来的是 Elasticsearch 相关部署、框架、性能优化的文章早已铺天盖地。

      初学者甚至会进入幻觉——“一键部署、导入数据、检索&聚合、动态扩展, So Easy,妈妈再也不用担心我的 Elastic 学习”!

      但,实际上呢?仅就 Elasticsearch 索引设计,请回答如下几个问题:

      • 每天几百 GB 增量实时数据的TB级甚至PB级别的大索引如何设计?
      • 分片数和副本数大小如何设计,才能提升 ES 集群的性能?
      • ES 的 Mapping 该如何设计,才能保证检索的高效?
      • 检索类型 term/match/matchphrase/querystring /match_phrase _prefix /fuzzy 那么多,设计阶段如何选型呢?
      • 分词该如何设计,才能满足复杂业务场景需求?
      • 传统数据库中的多表关联在 ES 中如何设计?......

      这么看来,没有那么 Easy,坑还是得一步步的踩出来的。

      正如携程架构师 WOOD 大叔所说“做搜索容易,做好搜索相当难!”,

      VIVO 搜索引擎架构师所说“ 熟练使用 ES 离做好搜索还差很远!”。

      本文主结合作者近千万级开发实战经验,和大家一起深入探讨一下Elasticsearch 索引设计......

      索引设计的重要性

      在美团写给工程师的十条精进原则中强调了“设计优先”。无数事实证明,忽略了前期设计,往往会带来很大的延期风险。并且未经评估的不当的设计会带来巨大的维护成本,后期不得不腾出时间,专门进行优化和重构。

      而 Elasticsearch 日渐成为大家非结构数据库的首选方案,项目前期良好的设计和评审是必须的,能给整个项目带来收益。

      索引层面的设计在 Elasticsearch 相关产品、项目的设计阶段的作用举重若轻。

      • 好的索引设计在整个集群规划中占据举足轻重的作用,索引的设计直接影响集群设计的好坏和复杂度。
      • 好的索引设计应该是充分结合业务场景的时间维度和空间维度,结合业务场景充分考量增、删、改、查等全维度设计的。
      • 好的索引设计是完全基于“设计先行,编码在后”的原则,前期会花很长时间,为的是后期工作更加顺畅,避免不必要的返工。

      1、PB 级别的大索引如何设计?

      单纯的普通数据索引,如果不考虑增量数据,基本上普通索引就能够满足性能要求。

      我们通常的操作就是:

      • 步骤 1:创建索引;
      • 步骤 2:导入或者写入数据;
      • 步骤 3:提供查询请求访问或者查询服务。

      1.1 大索引的缺陷

      如果每天亿万+的实时增量数据呢,基于以下几点原因,单个索引是无法满足要求的。在 360 技术访谈中也提到了大索引的设计的困惑。

      1.1.1 存储大小限制维度

      单个分片(Shard)实际是 Lucene 的索引,单分片能存储的最大文档数是:2,147,483,519 (= Integer.MAX_VALUE - 128)。如下命令能查看全部索引的分隔分片的文档大小:


      GET _cat/shardsapp_index 2 p STARTED 9443 2.8mb 127.0.0.1 Hk9wFwUapp_index 2 r UNASSIGNEDapp_index 3 p STARTED 9462 2.7mb 127.0.0.1 Hk9wFwUapp_index 3 r UNASSIGNEDapp_index 4 p STARTED 9520 3.5mb 127.0.0.1 Hk9wFwUapp_index 4 r UNASSIGNEDapp_index 1 p STARTED 9453 2.4mb 127.0.0.1 Hk9wFwUapp_index 1 r UNASSIGNEDapp_index 0 p STARTED 9365 2.3mb 127.0.0.1 Hk9wFwUapp_index 0 r UNASSIGNED

      1.1.2 性能维度

      当然一个索引很大的话,数据写入和查询性能都会变差。

      而高效检索体现在:基于日期的检索可以直接检索对应日期的索引,无形中缩减了很大的数据规模。

      比如检索:“2019-02-01”号的数据,之前的检索会是在一个月甚至更大体量的索引中进行。

      现在直接检索"index_2019-02-01"的索引,效率提升好几倍。

      1.1.3 风险维度

      一旦一个大索引出现故障,相关的数据都会受到影响。而分成滚动索引的话,相当于做了物理隔离。

      1.2 PB 级索引设计实现

      综上,结合实践经验,大索引设计建议:使用模板+Rollover+Curator动态创建索引。动态索引使用效果如下:


      index_2019-01-01-000001index_2019-01-02-000002index_2019-01-03-000003index_2019-01-04-000004index_2019-01-05-000005

      1.2.1 使用模板统一配置索引

      目的:统一管理索引,相关索引字段完全一致。

      1.2.2 使用 Rollver 增量管理索引

      目的:按照日期、文档数、文档存储大小三个维度进行更新索引。使用举例:


      POST /logs_write/_rollover{"conditions": {"max_age": "7d","max_docs": 1000,"max_size": "5gb"}}

      1.2.3 索引增量更新原理

      一图胜千言。

      索引更新的时机是:当原始索引满足设置条件的三个中的一个的时候,就会更新为新的索引。为保证业务的全索引检索,一般采用别名机制。

      在索引模板设计阶段,模板定义一个全局别名:用途是全局检索,如图所示的别名:indexall。每次更新到新的索引后,新索引指向一个用于实时新数据写入的别名,如图所示的别名:indexlatest。同时将旧索引的别名 index_latest 移除。

      别名删除和新增操作举例:


      POST /_aliases{"actions" : [{ "remove" : { "index" : "index_2019-01-01-000001", "alias" : "index_latest" } },{ "add" : { "index" : "index_2019-01-02-000002", "alias" : "index_latest" } }]}

      经过如上步骤,即可完成索引的更新操作。

      1.2.4 使用 curator 高效清理历史数据

      目的:按照日期定期删除、归档历史数据。

      一个大索引的数据删除方式只能使用 delete_by_query,由于 ES 中使用更新版本机制。删除索引后,由于没有物理删除,磁盘存储信息会不减反增。有同学就反馈 500GB+ 的索引 delete_by_query 导致负载增高的情况。

      而按照日期划分索引后,不需要的历史数据可以做如下的处理。

      • 删除——对应 delete 索引操作。

      • 压缩——对应 shrink 操作。

      • 段合并——对应 force_merge 操作。

      而这一切,可以借助:curator 工具通过简单的配置文件结合定义任务 crontab 一键实现。

      注意:7.X高版本借助iLM实现更为简单。

      举例,一键删除 30 天前的历史数据:

        [root@localhost .curator]# cat action.yml   actions:1:action: delete_indicesdescription: >-Delete indices older than30days (basedonindex name),forlogstash-prefixed indices.Ignore the errorifthe filter does not resultinanactionable list ofindices(ignore_empty_list) and exit cleanly.options:ignore_empty_list: Truedisable_action: Falsefilters:- filtertype: patternkind: prefixvalue: logs_- filtertype: agesource: namedirection: oldertimestring: '%Y.%m.%d'unit: daysunit_count: 30

      2、分片数和副本数如何设计?

      2.1 分片/副本认知

      • 1、分片:分片本身都是一个功能齐全且独立的“索引”,可以托管在集群中的任何节点上。

      数据切分分片的主要目的:

      (1)水平分割/缩放内容量 。 

      (2)跨分片(可能在多个节点上)分布和并行化操作,提高性能/吞吐量。 

      注意:分片一旦创建,不可以修改大小。

      • 2、副本:它在分片/节点出现故障时提供高可用性。

      副本的好处:因为可以在所有副本上并行执行搜索——因此扩展了搜索量/吞吐量。

      注意:副本分片与主分片存储在集群中不同的节点。副本的大小可以通过:number_of_replicas动态修改。

      2.2 分片和副本实战中设计

      最常见问题答疑

      2.2.1 问题 1:索引设置多少分片?

      Shard 大小官方推荐值为 20-40GB, 具体原理呢?Elasticsearch 员工 Medcl 曾经讨论如下:

      Lucene 底层没有这个大小的限制,20-40GB 的这个区间范围本身就比较大,经验值有时候就是拍脑袋,不一定都好使。

      Elasticsearch 对数据的隔离和迁移是以分片为单位进行的,分片太大,会加大迁移成本。

      一个分片就是一个 Lucene 的库,一个 Lucene 目录里面包含很多 Segment,每个 Segment 有文档数的上限,Segment 内部的文档 ID 目前使用的是 Java 的整型,也就是 2 的 31 次方,所以能够表示的总的文档数为Integer.MAXVALUE - 128 = 2^31 - 128 = 2147483647 - 1 = 2,147,483,519,也就是21.4亿条。

      同样,如果你不 forcemerge 成一个 Segment,单个 shard 的文档数能超过这个数。

      单个 Lucene 越大,索引会越大,查询的操作成本自然要越高,IO 压力越大,自然会影响查询体验。

      具体一个分片多少数据合适,还是需要结合实际的业务数据和实际的查询来进行测试以进行评估。

      综合实战+网上各种经验分享,梳理如下:

      • 第一步:预估一下数据量的规模。一共要存储多久的数据,每天新增多少数据?两者的乘积就是总数据量。

      • 第二步:预估分多少个索引存储。索引的划分可以根据业务需要。

      • 第三步:考虑和衡量可扩展性,预估需要搭建几台机器的集群。存储主要看磁盘空间,假设每台机器2TB,可用:2TB0.85(磁盘实际利用率)0.85(ES 警戒水位线)。

      • 第四步:单分片的大小建议最大设置为 30GB。此处如果是增量索引,可以结合大索引的设计部分的实现一起规划。

      前三步能得出一个索引的大小。分片数考虑维度:

      • 1)分片数 = 索引大小/分片大小经验值 30GB 。
      • 2)分片数建议和节点数一致。设计的时候1)、2)两者权衡考虑+rollover 动态更新索引结合。

      每个 shard 大小是按照经验值 30G 到 50G,因为在这个范围内查询和写入性能较好。

      经验值的探推荐阅读:

      Elasticsearch究竟要设置多少分片数?

      探究 | Elasticsearch集群规模和容量规划的底层逻辑

      2.2.2 问题 2:索引设置多少副本?

      结合集群的规模,对于集群数据节点 >=2 的场景:建议副本至少设置为 1。

      之前有同学出现过:副本设置为 0,长久以后会出现——数据写入向指定机器倾斜的情况。

      注意:

      单节点的机器设置了副本也不会生效的。副本数的设计结合数据的安全需要。对于数据安全性要求非常高的业务场景,建议做好:增强备份(结合 ES 官方备份方案)。

      3、Mapping 如何设计?

      3.1 Mapping 认知

      Mapping 是定义文档及其包含的字段的存储和索引方式的过程。例如,使用映射来定义:

      • 应将哪些字符串字段定义为全文检索字段;
      • 哪些字段包含数字,日期或地理位置;
      • 定义日期值的格式(时间戳还是日期类型等);
      • 用于控制动态添加字段的映射的自定义规则。

      3.2 设计 Mapping 的注意事项

      ES 支持增加字段 //新增字段

      PUT new_index{"mappings": {"_doc": {"properties": {"status_code": {"type":       "keyword"}}}}}
      • ES 不支持直接删除字段
      • ES 不支持直接修改字段
      • ES 不支持直接修改字段类型 如果非要做灵活设计,ES 有其他方案可以替换,借助reindex。但是数据量大会有性能问题,建议设计阶段综合权衡考虑。

      3.3 Mapping 字段的设置流程

      索引分为静态 Mapping(自定义字段)+动态 Mapping(ES 自动根据导入数据适配)。

      实战业务场景建议:选用静态 Mapping,根据业务类型自己定义字段类型。

      好处

      • 可控;
      • 节省存储空间(默认 string 是 text+keyword,实际业务不一定需要)。

      设置字段的时候,务必过一下如下图示的流程。根据实际业务需要,主要关注点:

      • 数据类型选型;
      • 是否需要检索;
      • 是否需要排序+聚合分析;
      • 是否需要另行存储。

      核心参数的含义,梳理如下:

      3.4 Mapping 建议结合模板定义

      索引 Templates——索引模板允许您定义在创建新索引时自动应用的模板。模板包括settings和Mappings以及控制是否应将模板应用于新索引。

      注意:模板仅在索引创建时应用。更改模板不会对现有索引产生影响。

      第1部分也有说明,针对大索引,使用模板是必须的。核心需要设置的setting(仅列举了实战中最常用、可以动态修改的)如下:

      • index.numberofreplicas 每个主分片具有的副本数。默认为 1(7.X 版本,低于 7.X 为 5)。
      • index.maxresultwindow 深度分页 rom + size 的最大值—— 默认为 10000。
      • index.refresh_interval 默认 1s:代表最快 1s 搜索可见;

      写入时候建议设置为 -1,提高写入性能;

      实战业务如果对实时性要求不高,建议设置为 30s 或者更高。

      3.5 包含 Mapping 的 template 设计万能模板

      以下模板已经在 7.2 验证 ok,可以直接拷贝修改后实战项目中使用。

      PUT _template/test_template{"index_patterns": ["test_index_*","test_*"],"settings": {"number_of_shards": 1,"number_of_replicas": 1,"max_result_window": 100000,"refresh_interval": "30s"},"mappings": {"properties": {"id": {"type": "long"},"title": {"type": "keyword"},"content": {"analyzer": "ik_max_word","type": "text","fields": {"keyword": {"ignore_above": 256,"type": "keyword"}}},"available": {"type": "boolean"},"review": {"type": "nested","properties": {"nickname": {"type": "text"},"text": {"type": "text"},"stars": {"type": "integer"}}},"publish_time": {"type": "date","format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"},"expected_attendees": {"type": "integer_range"},"ip_addr": {"type": "ip"},"suggest": {"type": "completion"}}}}

      4、分词的选型

      主要以 ik 来说明,最新版本的ik支持两种类型。ik_maxword 细粒度匹配,适用切分非常细的场景。ik_smart 粗粒度匹配,适用切分粗的场景。

      4.1 坑 1:分词选型

      实际业务中:建议适用ik_max_word分词 + match_phrase短语检索。

      原因:ik_smart有覆盖不全的情况,数据量大了以后,即便 reindex 能满足要求,但面对极大的索引的情况,reindex 的耗时我们承担不起。建议ik_max_word一步到位。

      4.2 坑 2:ik 要装集群的所有机器吗?

      建议:安装在集群的所有节点上。

      4.3 坑 3:ik 匹配不到怎么办?

      • 方案1:扩充 ik 开源自带的词库+动态更新词库;原生的词库分词数量级很小,基础词库尽量更大更全,网上搜索一下“搜狗词库“。

      动态更新词库:可以结合 mysql+ik 自带的更新词库的方式动态更新词库。

      更新词库仅对新创建的索引生效,部分老数据索引建议使用 reindex 升级处理。

      5、检索类型如何选型呢?

      前提:5.X 版本之后,string 类型不再存在,取代的是text和keyword类型。

      • text 类型作用:分词,将大段的文字根据分词器切分成独立的词或者词组,以便全文检索。

      适用于:email 内容、某产品的描述等需要分词全文检索的字段;

      不适用:排序或聚合(Significant Terms 聚合例外)

      • keyword 类型:无需分词、整段完整精确匹配。

      适用于:email 地址、住址、状态码、分类 tags。

      以一个实战例子说明:

      PUT zz_test{"mappings": {"doc": {"properties": {"title": {"type": "text","analyzer":"ik_max_word","fields": {"keyword": {"type": "keyword","ignore_above": 256}}}}}}}GET zz_test/_mapping
      PUT zz_test/doc/1{"title":"锤子加湿器官方致歉,难产后临时推迟一个月发货遭diss耍流氓"}

      POST zz_test/_analyze{"text": "锤子加湿器官方致歉,难产后临时推迟一个月发货遭diss耍流氓","analyzer": "ik_max_word"}

      ik_max_word的分词结果如下:

      锤子、锤、子、加湿器、湿、器官、官方、方、致歉、致、歉、难产、产后、后、临时、临、时、推迟、迟、一个、 一个、 一、个月、 个、 月、 发货、发、货、遭、diss、耍流氓、耍、流氓、氓。

      5.1 term 精确匹配

      • 核心功能:不受到分词器的影响,属于完整的精确匹配。
      • 应用场景:精确、精准匹配。
      • 适用类型:keyword。
      • 举例:term 最适合匹配的类型是 keyword,如下所示的精确完整匹配:
      POST zz_test/_search{"query": {"term": {"title.keyword": "锤子加湿器官方致歉,难产后临时推迟一个月发货遭diss耍流氓"}}}
      • 注意:如下是匹配不到结果的。
      POST zz_test/_search{"query": {"term": {"title": "锤子加湿器"}}}
      • 原因:对于 title 中的锤子加湿器,term 不会做分词拆分匹配的。且 ik_max_word 分词也是没有“锤子加湿器”这组关键词的。

      5.2 prefix 前缀匹配

      • 核心功能:前缀匹配。
      • 应用场景:前缀自动补全的业务场景。
      • 适用类型:keyword。

      如下能匹配到文档 id 为 1 的文章。

      POST zz_test/_search{"query": {"prefix": {"title.keyword": "锤子加湿器"}}}

      5.3 wildcard 模糊匹配

      • 核心功能:匹配具有匹配通配符表达式 keyword 类型的文档。支持的通配符:*,它匹配任何字符序列(包括空字符序列);?,它匹配任何单个字符。
      • 应用场景:请注意,选型务必要慎重!此查询可能很慢多组关键次的情况下可能会导致宕机,因为它需要遍历多个术语。为了防止非常慢的通配符查询,通配符不能以任何一个通配符*或?开头。

      • 适用类型:keyword。

      如下匹配,类似 MySQL 中的通配符匹配,能匹配所有包含加湿器的文章。

      POST zz_test/_search{"query": {"wildcard": {"title.keyword": "*加湿器*"}}}

      5.4 match 分词匹配

      • 核心功能:全文检索,分词词项匹配。
      • 应用场景:实际业务中较少使用,原因:匹配范围太宽泛,不够准确。
      • 适用类型:text。
      • 如下示例,title 包含"锤子"和“加湿器”的都会被检索到。
      POST zz_test/_search{"profile": true,"query": {"match": {"title": "锤子加湿器"}}}

      5.5 match_phrase 短语匹配

      • 核心功能:match_phrase 查询首先将查询字符串解析成一个词项列表,然后对这些词项进行搜索; 只保留那些包含 全部 搜索词项,且 位置"position" 与搜索词项相同的文档。

      • 应用场景:业务开发中 90%+ 的全文检索都会使用 match_phrase 或者 query_string 类型,而不是 match。

      • 适用类型:text。

      • 注意:

      POST zz_test/_analyze{"text": "锤子加湿器","analyzer": "ik_max_word"}
      • 分词结果:

      锤子, 锤,子, 加湿器, 湿,器。而:id为1的文档的分词结果:锤子, 锤, 子, 加湿器, 湿, 器官。所以,如下的检索是匹配不到结果的。

      POST zz_test/_search{"query": {"match_phrase": {"title": "锤子加湿器"}}}

      如果想匹配到,怎么办呢?这里可以字词组合索引的形式。

      推荐阅读:

      探究 | 明明存在,怎么搜索不出来呢?

      5.6 multi_match 多组匹配

      • 核心功能:match query 针对多字段的升级版本。
      • 应用场景:多字段检索。
      • 适用类型:text。
      • 举例:
      POST zz_test/_search{"query": {"multi_match": {"query": "加湿器","fields": ["title","content"]}}}

      5.7 query_string 类型

      • 核心功能:支持与或非表达式+其他N多配置参数。
      • 应用场景:业务系统需要支持自定义表达式检索。
      • 适用类型:text。
      POST zz_test/_search{"query": {"query_string": {"default_field": "title","query": "(锤子 AND 加湿器) OR (官方 AND 道歉)"}}}

      5.8 bool 组合匹配

      • 核心功能:多条件组合综合查询。
      • 应用场景:支持多条件组合查询的场景。
      • 适用类型:text 或者 keyword。一个 bool 过滤器由三部分组成:
      {"bool" : {"must" :     [],"should" :   [],"must_not" : [],"filter":    []}}
      • must ——所有的语句都 必须(must) 匹配,与 AND 等价。
      • must_not ——所有的语句都 不能(must not) 匹配,与 NOT 等价。
      • should ——至少有一个语句要匹配,与 OR 等价。
      • filter——必须匹配,运行在非评分&过滤模式。
      小结:

      6、多表关联如何设计?

      6.1 为什么会有多表关联

      多表关联是被问的最多的问题之一。几乎每周都会被问到。

      主要原因:常规基于关系型数据库开发,多多少少都会遇到关联查询。而关系型数据库设计的思维很容易带到 ES 的设计中。

      6.2 多表关联如何实现

      方案一:多表关联视图,视图同步 ES

      MySQL 宽表导入 ES,使用 ES 查询+检索。适用场景:基础业务都在 MySQL,存在几十张甚至几百张表,准备同步到 ES,使用 ES 做全文检索。

      将数据整合成一个宽表后写到 ES,宽表的实现可以借助关系型数据库的视图实现。

      宽表处理在处理一对多、多对多关系时,会有字段冗余问题,如果借助:logstash_input_jdbc,关系型数据库如 MySQL 中的每一个字段都会自动帮你转成 ES 中对应索引下的对应 document 下的某个相同字段下的数据。

      • 步骤 1:提前关联好数据,将关联的表建立好视图,一个索引对应你的一个视图,并确认视图中数据的正确性。

      • 步骤 2:ES 中针对每个视图定义好索引名称及 Mapping。

      • 步骤 3:以视图为单位通过 logstash_input_jdbc 同步到 ES 中。

      方案二:1 对 1 同步 ES

      MySQL+ES 结合,各取所长。适用场景:关系型数据库全量同步到 ES 存储,没有做冗余视图关联。

      ES 擅长的是检索,而 MySQL 才擅长关系管理。

      所以可以考虑二者结合,使用 ES 多索引建立相同的别名,针对别名检索到对应 ID 后再回 MySQL 通过关联 ID join 出需要的数据。

      方案三:使用 Nested 做好关联

      适用场景:1 对少量的场景。

      举例:有一个文档描述了一个帖子和一个包含帖子上所有评论的内部对象评论。可以借助 Nested 实现。

      Nested 类型选型——如果需要索引对象数组并保持数组中每个对象的独立性,则应使用嵌套 Nested 数据类型而不是对象 Oject 数据类型。

      当使用嵌套文档时,使用通用的查询方式是无法访问到的,必须使用合适的查询方式(nested query、nested filter、nested facet等),很多场景下,使用嵌套文档的复杂度在于索引阶段对关联关系的组织拼装。

      方案四:使用ES6.X+ 父子关系 Join 做关联

      适用场景:1对多量的场景。

      举例:1 个产品和供应商之间是1对N的关联关系。

      Join 类型:join 数据类型是一个特殊字段,用于在同一索引的文档中创建父/子关系。关系部分定义文档中的一组可能关系,每个关系是父名称和子名称。

      当使用父子文档时,使用has_child 或者has_parent做父子关联查询。

      方案三、方案四选型对比:

      注意:方案三&方案四选型必须考虑性能问题。文档应该尽量通过合理的建模来提升检索效率。

      Join 类型应该尽量避免使用。nested 类型检索使得检索效率慢几倍,父子Join 类型检索会使得检索效率慢几百倍。

      尽量将业务转化为没有关联关系的文档形式,在文档建模处多下功夫,以提升检索效率。

      干货 | 论Elasticsearch数据建模的重要性

      干货 | Elasticsearch多表关联设计指南

      小结

      7、实战中遇到过的坑

      如果能重来,我会如何设计 Elasticsearch 系统?

      来自累计近千万实战项目设计的思考。

      • 坑1: 数据清洗一定发生在写入 es 之前!而不是请求数据后处理,拿势必会降低请求速度和效率。

      • 坑2:高亮不要重复造轮子,用原生就可以。

      • 坑3:让 es 做他擅长的事,检索+不复杂的聚合,否则数据量+复杂的业务逻辑大会有性能问题。

      • 坑4:设计的工作必须不要省!快了就是慢了,否则无休止的因设计缺陷引发的 bug 会增加团队的戳败感!

      • 坑5:在给定时间的前提下,永远不会有完美的设计,必须相对合理的设计+重构结合,才会有相对靠谱的系统。

      • 坑6:SSD 能提升性能,但如果系统业务逻辑非常负责,换了 SSD 未必达到预期。

      • 坑7:由于 Elasticsearch 不支持事务 ACID 特性,数据库作为实时数据补充,对于实时数据要求严格的场景,必须同时采取双写或者同步的方式。这样,一旦实时数据出现不一致,可以通过数据库进行同步递增更新。

      8、小结

      本文从选题和撰写历时2周+的时间,期间反复梳理了开发过程中遇到的问题、社区/QQ 群/知识星球等中大家提问的问题。

      将TOP N 大家最关注的问题也是实战中得得确确容易混淆的问题梳理出来,目的是:让更多的后来人看到,不要再走一遍弯路。

      相信经过几天梳理的实战文档,能给你带来帮助!感谢您的反馈和交流。

      推荐阅读:

      1、百亿级日志系统架构设计及优化 http://t.cn/RsGCUGs 

      2.探讨理想的Elasticsearch索引设计原则。http://t.cn/Rusfb0U

      公众号:铭毅天下 

      博客:elastic.blog.csdn.net

      更短时间更快习得更多干货!

      HBase如何合理设置客户端Write Buffer - 大圆那些事 - 博客园

      $
      0
      0

      作者: 大圆那些事| 转载请以超链接形式标明文章原始出处和作者信息
      网址: http://www.cnblogs.com/panfeng412/archive/2012/10/16/how-to-use-hbase-client-write-buffer.html

      HBase客户端API提供了Write Buffer的方式,即批量提交一批Put对象到HBase服务端。本文将结合HBase相关源码,对其进行深入介绍,分析如何在实际项目中合理设置和使用它。

      1. 什么时候需要Write Buffer?

      默认情况下,一次Put操作即要与Region Server执行一次RPC操作,其执行过程可以被拆分为以下三个部分:

      • T1:RTT(Round-Trip Time),即网络往返时延,它指从客户端发送数据开始,到客户端收到来自服务端的确认,总共经历的时延,不包括数据传输的时间;
      • T2:数据传输时间,即Put所操作的数据在客户端与服务端之间传输所消耗的时间开销,当数据量大的时候,T2的时间开销不容忽略;
      • T3:服务端处理时间,对于Put操作,即写入WAL日志(如果设置了WAL标识为true)、更新MemStore等。

      其中,T2和T3都是不可避免的时间开销,那么能不能减少T1呢?假设我们将多次Put操作打包起来一次性提交到服务端,则可以将T1部分的总时间从T1 * N降低为T1,其中T1指的是单次RTT时间,N为Put的记录条数。

      正是出于上述考虑,HBase为用户提供了客户端缓存批量提交的方式(即Write Buffer)。假设RTT的时间较长,如1ms,则该种方式能够显著提高整个集群的写入性能。

      那么,什么场景下适用于该种模式呢?下面简单分析一下:

      • 如果Put提交的是小数据(如KB级别甚至更小)记录,那么T2很小,因此,通过该种模式减少T1的开销,能够明显提高写入性能。
      • 如果Put提交的是大数据(如MB级别)记录,那么T2可能已经远大于T1,此时T1与T2相比可以被忽略,因此,使用该种模式并不能得到很好的性能提升,不建议通过增大Write Buffer大小来使用该种模式。

      2. 如何配置使用Write Buffer?

      如果要启动Write Buffer模式,则调用HTable的以下API将auto flush设置为false:

      voidsetAutoFlush(booleanautoFlush)

      默认配置下,Write Buffer大小为2MB,可以根据应用实际情况,通过以下任意方式进行自定义:

      1) 调用HTable接口设置,仅对该HTable对象起作用:

      voidsetWriteBufferSize(longwriteBufferSize)throwsIOException

      2) 在hbase-site.xml中配置,所有HTable都生效(下面设置为5MB):

      <property><name>hbase.client.write.buffer</name><value>5242880</value></property>

      该种模式下向服务端提交的时机分为显式和隐式两种情况:

      1) 显式提交:用户调用flushCommits()进行提交;

      2) 隐式提交:当Write Buffer满了,客户端会自动执行提交;或者调用了HTable的close()方法时无条件执行提交操作。

      3. 如何确定每次flushCommits()时实际的RPC次数?

      客户端提交后,所有的Put操作可能涉及不同的行,然后客户端负责将这些Put对象根据row key按照region server分组,再按region server打包后提交到region server,每个region server做一次RPC请求。如下图所示:

       

       

      4. 如何确定每次flushCommits()时提交的记录条数?

      下面我们先从HBase存储原理层面“粗略”分析下HBase中的一条Put记录格式:

      HBase中Put对象的大小主要由若干个KeyValue对的大小决定(Put继承自org/apache/hadoop/hbase/client/Mutation.java,具体见Mutation的代码所示),而KeyValue类中自带的字段占用约50~60 bytes(参考源码:org/apache/hadoop/hbase/KeyValue.java),那么客户端Put一行数据时,假设column qualifier个数为N,row key长度为L1 bytes,value总长度为L2 bytes,则该Put对象占用大小可按以下公式预估:

      Put Size = ((50~60) + L1) * N + L2) bytes

      下面我们通过对HBase的源码分析来进一步验证以上理论估算值:

      HBase客户端执行put操作后,会调用put.heapSize()累加当前客户端buffer中的数据,满足以下条件则调用flushCommits()将客户端数据提交到服务端:

      1)每次put方法调用时可能传入的是一个List<Put>,此时每隔DOPUT_WB_CHECK条(默认为10条),检查当前缓存数据是否超过writeBufferSize,超过则强制执行刷新;

      2)autoFlush被设置为true,此次put方法调用后执行一次刷新;

      3)autoFlush被设置为false,但当前缓存数据已超过设定的writeBufferSize,则执行刷新。

      privatevoiddoPut(finalList<Put> puts)throwsIOException {intn = 0;for(Put put : puts) {
                  validatePut(put);
                  writeBuffer.add(put);
                  currentWriteBufferSize+=put.heapSize();//we need to periodically see if the writebuffer is full instead//of waiting until the end of the Listn++;if(n % DOPUT_WB_CHECK == 0&& currentWriteBufferSize >writeBufferSize) {
                      flushCommits();
                  }
              }if(autoFlush || currentWriteBufferSize >writeBufferSize) {
                  flushCommits();
              }
          }

      由上述代码可见,通过put.heapSize()累加客户端的缓存数据,作为判断的依据;那么,我们可以编写一个简单的程序生成Put对象,调用其heapSize()方法,就能得到一行数据实际占用的客户端缓存大小(该程序需要传递上述三个变量:N,L1,L2作为参数):

      importorg.apache.hadoop.hbase.client.Put;importorg.apache.hadoop.hbase.util.Bytes;publicclassPutHeapSize {/***@paramargs*/publicstaticvoidmain(String[] args) {if(args.length != 3) {
                  System.out.println("Invalid number of parameters: 3 parameters!");
                  System.exit(1);
              }intN = Integer.parseInt(args[0]);intL1 = Integer.parseInt(args[1]);intL2 = Integer.parseInt(args[2]);byte[] rowKey =newbyte[L1];byte[] value =null;
              Put put=newPut(rowKey);for(inti = 0; i < N; i++) {
                  put.add(Bytes.toBytes("cf"), Bytes.toBytes("c" +i), value);
              }
              System.out.println("Put Size: " + (put.heapSize() + L2) + " bytes");
          }
      }

      该程序可以用来预估当前设置的write buffer可以一次性批量提交的记录数:

      Puts Per Commit = Write Buffer Size / Put Size

      更进一步地,如果知道业务中的每秒产生的数据量,就可知道客户端大概多长时间会隐式调用flushCommits()向服务端提交一次;同时也可反过来根据数据实时刷新频率调整Write Buffer大小。

      5. Write Buffer有什么潜在的问题?

      首先,Write Buffer存在于客户端的本地内存中,那么当客户端运行出现问题时,会导致在Write Buffer中未提交的数据丢失;由于HBase服务端还未收到这些数据,因此也无法通过WAL日志等方式进行数据恢复。

      其次,Write Buffer方式本身会占用客户端和HBase服务端的内存开销,具体见下节的详细分析。

      6. 如何预估Write Buffer占用的内存?

      客户端通过Write Buffer方式提交的话,会导致客户端和服务端均有一定的额外内存开销,Write Buffer Size越大,则占用的内存越大。客户端占用的内存开销可以粗略使用以下公式预估:

      hbase.client.write.buffer * number of HTableobjectforwriting

      而对于服务端来说,可以使用以下公式预估占用的Region Server总内存开销:

      hbase.client.write.buffer * hbase.regionserver.handler.count * number of region server

      其中,hbase.regionserver.handler.count为每个Region Server上配置的RPC Handler线程数。

       

      HBase BulkLoad批量写入数据实战 - 哥不是小萝莉 - 博客园

      $
      0
      0

      1.概述

      在进行数据传输中,批量加载数据到HBase集群有多种方式,比如通过HBase API进行批量写入数据、使用Sqoop工具批量导数到HBase集群、使用MapReduce批量导入等。这些方式,在导入数据的过程中,如果数据量过大,可能耗时会比较严重或者占用HBase集群资源较多(如磁盘IO、HBase Handler数等)。今天这篇博客笔者将为大家分享使用HBase BulkLoad的方式来进行海量数据批量写入到HBase集群。

      2.内容

      在使用BulkLoad之前,我们先来了解一下HBase的存储机制。HBase存储数据其底层使用的是HDFS来作为存储介质,HBase的每一张表对应的HDFS目录上的一个文件夹,文件夹名以HBase表进行命名(如果没有使用命名空间,则默认在default目录下),在表文件夹下存放在若干个Region命名的文件夹,Region文件夹中的每个列簇也是用文件夹进行存储的,每个列簇中存储就是实际的数据,以HFile的形式存在。路径格式如下:

      /hbase/data/default/<tbl_name>/<region_id>/<cf>/<hfile_id>

      2.1 实现原理

      按照HBase存储数据按照HFile格式存储在HDFS的原理,使用MapReduce直接生成HFile格式的数据文件,然后在通过RegionServer将HFile数据文件移动到相应的Region上去。流程如下图所示:

      2.2. 生成HFile文件

      HFile文件的生成,可以使用MapReduce来进行实现,将数据源准备好,上传到HDFS进行存储,然后在程序中读取HDFS上的数据源,进行自定义封装,组装RowKey,然后将封装后的数据在回写到HDFS上,以HFile的形式存储到HDFS指定的目录中。实现代码如下:

      复制代码
      /**
       * Read DataSource from hdfs & Gemerator hfile.
       * 
       * @author smartloli.
       *
       *         Created by Aug 19, 2018
       */
      public class GemeratorHFile2 {
          static class HFileImportMapper2 extends Mapper<LongWritable, Text, ImmutableBytesWritable, KeyValue> {
              protected final String CF_KQ = "cf";
      
              @Override
              protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
                  String line = value.toString();
                  System.out.println("line : " + line);
                  String[] datas = line.split(" ");
                  String row = new Date().getTime() + "_" + datas[1];
                  ImmutableBytesWritable rowkey = new ImmutableBytesWritable(Bytes.toBytes(row));
                  KeyValue kv = new KeyValue(Bytes.toBytes(row), this.CF_KQ.getBytes(), datas[1].getBytes(), datas[2].getBytes());
                  context.write(rowkey, kv);
              }
          }
      
          public static void main(String[] args) {
              if (args.length != 1) {
                  System.out.println("<Usage>Please input hbase-site.xml path.</Usage>");
                  return;
              }
              Configuration conf = new Configuration();
              conf.addResource(new Path(args[0]));
              conf.set("hbase.fs.tmp.dir", "partitions_" + UUID.randomUUID());
              String tableName = "person";
              String input = "hdfs://nna:9000/tmp/person.txt";
              String output = "hdfs://nna:9000/tmp/pres";
              System.out.println("table : " + tableName);
              HTable table;
              try {
                  try {
                      FileSystem fs = FileSystem.get(URI.create(output), conf);
                      fs.delete(new Path(output), true);
                      fs.close();
                  } catch (IOException e1) {
                      e1.printStackTrace();
                  }
      
                  Connection conn = ConnectionFactory.createConnection(conf);
                  table = (HTable) conn.getTable(TableName.valueOf(tableName));
                  Job job = Job.getInstance(conf);
                  job.setJobName("Generate HFile");
      
                  job.setJarByClass(GemeratorHFile2.class);
                  job.setInputFormatClass(TextInputFormat.class);
                  job.setMapperClass(HFileImportMapper2.class);
                  FileInputFormat.setInputPaths(job, input);
                  FileOutputFormat.setOutputPath(job, new Path(output));
      
                  HFileOutputFormat2.configureIncrementalLoad(job, table);
                  try {
                      job.waitForCompletion(true);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  } catch (ClassNotFoundException e) {
                      e.printStackTrace();
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
      
          }
      }
      复制代码

      在HDFS目录/tmp/person.txt中,准备数据源如下:

      1 smartloli 100
      2 smartloli2 101
      3 smartloli3 102

      然后,将上述代码编译打包成jar,上传到Hadoop集群进行执行,执行命令如下:

      hadoop jar GemeratorHFile2.jar /data/soft/new/apps/hbaseapp/hbase-site.xml

      如果在执行命令的过程中,出现找不到类的异常信息,可能是本地没有加载HBase依赖JAR包,在当前用户中配置如下环境变量信息:

      export HADOOP_CLASSPATH=$HBASE_HOME/lib/*:classpath

      然后,执行source命令使配置的内容立即生生效。

      2.3. 执行预览

      在成功提交任务后,Linux控制台会打印执行任务进度,也可以到YARN的资源监控界面查看执行进度,结果如下所示:

      等待任务的执行,执行完成后,在对应HDFS路径上会生成相应的HFile数据文件,如下图所示:

      2.4 使用BulkLoad导入到HBase

      然后,在使用BulkLoad的方式将生成的HFile文件导入到HBase集群中,这里有2种方式。一种是写代码实现导入,另一种是使用HBase命令进行导入。

      2.4.1 代码实现导入

      通过LoadIncrementalHFiles类来实现导入,具体代码如下:

      复制代码
      /**
      * Use BulkLoad inport hfile from hdfs to hbase.
      * 
      * @author smartloli.
      *
      * Created by Aug 19, 2018
      */
      public class BulkLoad2HBase {
      
          public static void main(String[] args) throws Exception {
              if (args.length != 1) {
                  System.out.println("<Usage>Please input hbase-site.xml path.</Usage>");
                  return;
              }
              String output = "hdfs://cluster1/tmp/pres";
              Configuration conf = new Configuration();
              conf.addResource(new Path(args[0]));
              HTable table = new HTable(conf, "person");
              LoadIncrementalHFiles loader = new LoadIncrementalHFiles(conf);
              loader.doBulkLoad(new Path(output), table);
          }
      }
      复制代码

      执行上述代码,运行结果如下:

      2.4.2 使用HBase命令进行导入

      先将生成好的HFile文件迁移到目标集群(即HBase集群所在的HDFS上),然后在使用HBase命令进行导入,执行命令如下:

      # 先使用distcp迁移hfile
      hadoop distcp -Dmapreduce.job.queuename=queue_1024_01 -update -skipcrccheck -m 10 /tmp/pres hdfs://nns:9000/tmp/pres
      
      # 使用bulkload方式导入数据
      hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles /tmp/pres person

      最后,我们可以到指定的RegionServer节点上查看导入的日志信息,如下所示为导入成功的日志信息:

      2018-08-19 16:30:34,969 INFO  [B.defaultRpcServer.handler=7,queue=1,port=16020] regionserver.HStore: Successfully loaded store file hdfs://cluster1/tmp/pres/cf/7b455535f660444695589edf509935e9 into store cf (new location: hdfs://cluster1/hbase/data/default/person/2d7483d4abd6d20acdf16533a3fdf18f/cf/d72c8846327d42e2a00780ac2facf95b_SeqId_4_)

      2.5 验证

      使用BulkLoad方式导入数据后,可以进入到HBase集群,使用HBase Shell来查看数据是否导入成功,预览结果如下:

      3.总结

      本篇博客为了演示实战效果,将生成HFile文件和使用BulkLoad方式导入HFile到HBase集群的步骤进行了分解,实际情况中,可以将这两个步骤合并为一个,实现自动化生成与HFile自动导入。如果在执行的过程中出现RpcRetryingCaller的异常,可以到对应RegionServer节点查看日志信息,这里面记录了出现这种异常的详细原因。

      4.结束语

      这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

      Viewing all 15907 articles
      Browse latest View live


      <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>