月度归档: 2021 年 5 月

常用文件服务器

常用文件服务器:

(主要说一些基础知识,配置过程简单,可以百度下,小编主要整理下知识点)

一、FTP 服务器

软件包: vsftpd

FTP端口: 控制端口 21/tcp

数据端口 20/tcp (主动模式)

配置文件: /etc/vsftpd/vsftpd.conf

FTP的主被动模式:

1、主动模式首先,FTP客户端随机开启一个大于1024的端口P(2000)并与服务器的21端口建立连接,然后开放一个P+1号的端口(2001)进行监听,同时向服务器发出PORT 2001命令(PORT)命令包括客户端用什么端口接收数据)。服务器在传送数据的时个,通过自己的TCP20端口发送数据,因此FTP必须和客户端建立一具新的连接用于数据传输。

2、被动模式在被动模式下建立控制通道类似于在主动模式下通道的操作:FTP客户端随机开启一个大于1024的端口P(1999)向服务器的21端口发起连接,同时会开启P+1号端口(2000)然后向服务器端发关PASV命令,通知服务器处于被动模式,服务器收到命令后,开放一个大于1024的端口P(1213)进行监听,然后用PORT P命令通知客户端,自己的数据端口是1213,客户端收到命令后,通过2000端口连接服务器的端品1213,然后在两端口之间进行数据传输。

主要配置文件:/etc/vsftpd/vsftpd.conf文件:

[root@localhost ~]# vim /etc/vsftpd/vsftpd.conf
anonymous_enable=YES //是否允许匿名用户登录
local_enable=YES //是否允许本地用户登录
write_enable=YES //是否允许写(全局)
local_umask=022 //控制本地用户上传文件的默认权限,umask表示要减掉的权限
anon_umask=077 //控制匿名用户上传文件的默认权限

chroot: 锁定本地用户HOME
方法一:部分用户chroot
chroot_list_enable=YES
chroot_list_file=/etc/vsftpd/chroot_list
方法二:所有本地用户chroot
chroot_local_user=YES

anon_max_rate=500000 //匿名用户限速
local_max_rate=80000 //本地用户限速
max_clients=500 //ftp*大连接数
max_per_ip=2 //单个IP*大连接数,线程数

local_root=/ftproot //指定本地用户访问的root目录
anon_root=/anonroot //指定匿名用户访问的root目录
相关文件:

/etc/vsftpd/ftpusers //黑名单

/etc/vsftpd/user_list //白名单

二、NFS文件服务

NFS:Network File System 网络文件系统,Unix系统之间共享文件的一种协议,允许网络中的计算机之间通过TCP/IP网络共享资源,但是都是明文发送,安全性能一般(建议只在局域网下使用)

NFS 的客户端主要为Linux

支持多节点同时挂载以及并发写入

首先介绍 一个服务:RPC(Remote Preceduce Call )远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP/IP或UDP,为通信程序之间携带信息数据。RPC将原来的本地调用转变为调用远端的服务器上的方法,给系统的处理能力和吞吐量带来了近似于无限制提升的可能。在OSI网络通信模型中,RPC跨域了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

在NFS上,RPC *主要的功能就是在指定每个 NFS 功能所对应的 port number ,并且回报给客户端,让客户端可以连结到正确的port上去。

NFS原理:

NFS Server服务器上设定/data目录被分享,而客户端可以通过internet将/data目录挂载到本地的挂载点(常用mnt)后,客户端就可以进入挂载点目录进行文件的读写。NFS客户端所开放的端口是随机的我们不知道,那该怎么办呢?那是不是就不能进行数据传输了呢?答案当然是NO了那就需要另外一种服务帮他分配port了,服务是什么呢?那就是RPC服务了!

(简单来说,就是在服务器上共享文件夹,然后客户端本地挂载使用)

主要配置文件:

/etc/exports :默认也是为空的,自己手动创建内容:

# vim /etc/exports /data 192.168.95.0/24(rw,sync,no_root_squash)

权限参数:

rw //可读写的权限

ro //只读的权限

sync(同步) //资料同步写入到内存与硬盘中(慢,不容易丢数据)

rsync(异步) //资料会暂存于内存中,而不是写入硬盘(快,容易丢数据)

no_root_squash //客户端用root用户访问共享文件夹是,root用户不会映射成匿名用户

root_squash //…

all_squash //…

anonuid = XXX //指定匿名用户UID

anongid = XXX //…

insecure //NFS通过1024以上端口发送

secure //…

hide //在NFS共享目录中不共享其子目录

no_hide //…

wdelay //如果多个用户写入NFS目录,则归组写入(默认)

no_wdelay //…

subtree_check //在共享/usr/bin之类的子目录时,强制NFS检查父目录的权限(默认)

no_subtree_check //…

启动服务:

systemctl restart rpcbind

systemctl restart nfs

命令介绍:

exportfs

showmount

三、samba(不常用)

CIFS: Common Internet File System Windows和Unix系统之间共享文件的一种协议

CIFS:客户端主要是Windows

支持多节点同时挂载以及并发写入

 

Samba是在Linux和UNIX系统上实现SMB协议的一个免费软件,由服务器及客户端程序构成。它为局域网内的不同计算机之间提供文件及打印机等资源的共享服务。

​Samba*大的功能就是可以用于Linux与windows系统直接的文件共享和打印共享

主要配置文件:/etc/samba/smb.conf

smb.conf文件的配置内容

[global]:全局设置

[homes]:用户目录共享设置

[printers]:打印机共享设置

[myshare]:自定义名称的共享目录设置

[root@samba ~]# vim /etc/samba/smb.conf
[data]
path = /data
;valid users = alice jack
;hosts allow = 172.16.30.
writable = yes
FTP和NFS为主要常用的文件服务器,samba 了解就行,前面两个要求原理,配置过程等都得熟悉

git rebase详解

转载自:https://www.codercto.com/a/45325.html

使用 Git 已经好几年了,却始终只是熟悉一些常用的操作。对于 Git Rebase 却很少用到,直到这一次,不得不用。

一、起因

上线构建的过程中扫了一眼代码变更,突然发现, commit 提交竟然多达 62 次。我们来看看都提交了什么东西:

这一次彻底搞懂 Git Rebase

这里我们先不说 git 提交规范,就单纯这么多次无用的 commit 就很让人不舒服。可能很多人觉得无所谓,无非是多了一些提交纪录。

然而,并非如此,你可能听过破窗效应,编程也是如此!

二、导致问题

1.不利于代码 review
设想一下,你要做 code review ,结果一个很小的功能,提交了 60 多次,会不会有一些崩溃?

2.会造成分支污染

你的项目充满了无用的 commit 纪录,如果有一天线上出现了紧急问题,你需要回滚代码,却发现海量的 commit 需要一条条来看。

遵循项目规范才能提高团队协作效率,而不是随心所欲。

三、如何合并多次提交纪录?

基于上面所说问题,我们不难想到:每一次功能开发, 对多个 commit 进行合并处理。

这时候就需要用到 git rebase 了。这个命令没有太难,不常用可能源于不熟悉,所以我们来通过示例学习一下。

1.我们来合并*近的 4 次提交纪录,执行:

git rebase -i HEAD~4

2.这时候,会自动进入 vi 编辑模式:

s cacc52da add: qrcode
s f072ef48 update: indexeddb hack
s 4e84901a feat: add indexedDB floder
s 8f33126c feat: add test2.js

# Rebase 5f2452b2..8f33126c onto 5f2452b2 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

有几个命令需要注意一下:

  • p, pick = use commit
  • r, reword = use commit, but edit the commit message
  • e, edit = use commit, but stop for amending
  • s, squash = use commit, but meld into previous commit
  • f, fixup = like “squash”, but discard this commit’s log message
  • x, exec = run command (the rest of the line) using shell
  • d, drop = remove commit

按照如上命令来修改你的提交纪录:

s cacc52da add: qrcode
s f072ef48 update: indexeddb hack
s 4e84901a feat: add indexedDB floder
p 8f33126c feat: add test2.js

3.如果保存的时候,你碰到了这个错误:

error: cannot 'squash' without a previous commit

注意不要合并先前提交的东西,也就是已经提交远程分支的纪录。

4.如果你异常退出了 vi 窗口,不要紧张:

git rebase --edit-todo

这时候会一直处在这个编辑的模式里,我们可以回去继续编辑,修改完保存一下:

git rebase --continue

5.查看结果

git log

三次提交合并成了一次,减少了无用的提交信息。

这一次彻底搞懂 Git Rebase

四、Rebase 的另外一种使用场景:分支合并

1.我们先从 master 分支切出一个 dev 分支,进行开发:

git:(master) git checkout -b feature1

这一次彻底搞懂 Git Rebase

2.这时候,你的同事完成了一次 hotfix ,并合并入了 master 分支,此时 master 已经*于你的 feature1 分支了:

这一次彻底搞懂 Git Rebase

3.恰巧,我们想要同步 master 分支的改动,首先想到了 merge ,执行:

git:(feature1) git merge master

这一次彻底搞懂 Git Rebase 图中绿色的点就是我们合并之后的结果,执行:

git:(feature1) git log

就会在记录里发现一些 merge 的信息,但是我们觉得这样污染了 commit 记录,想要保持一份干净的 commit ,怎么办呢?这时候, git rebase 就派上用场了。

4.让我们来试试 git rebase ,先回退到同事 hotfix 后合并 master 的步骤:

这一次彻底搞懂 Git Rebase

5.使用 rebase 后来看看结果:

git:(feature1) git rebase master

这里补充一点: rebase 做了什么操作呢?

首先, git 会把 feature1 分支里面的每个 commit 取消掉;

其次,把上面的操作临时保存成 patch 文件,存在 .git/rebase 目录下;

然后,把 feature1 分支更新到*新的 master 分支;

*后,把上面保存的 patch 文件应用到 feature1 分支上;

这一次彻底搞懂 Git Rebase

从 commit 记录我们可以看出来, feature1 分支是基于 hotfix 合并后的 master ,自然而然的成为了**的分支,而且没有 merge 的 commit 记录,是不是感觉很舒服了。

6.在 rebase 的过程中,也许会出现冲突 conflict 。在这种情况, git 会停止 rebase 并会让你去解决冲突。在解决完冲突后,用 git add 命令去更新这些内容。

注意,你无需执行 git-commit,只要执行 continue

git rebase --continue

这样 git 会继续应用余下的 patch 补丁文件。

7.在任何时候,我们都可以用 --abort 参数来终止 rebase 的行动,并且分支会回到 rebase 开始前的状态。

git rebase —abort

 

谈谈对 Canal( 增量数据订阅与消费 )的理解

 

之前说了,大数据平台技术栈 (可点击查看),今天就来说说其中的Cannal

 

概述

 

canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。

 

起源:早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务,从此开启了一段新纪元。

 

基于日志增量订阅&消费支持的业务:

 

  • 数据库镜像
  • 数据库实时备份
  • 多级索引 (卖家和买家各自分库索引)
  • search build
  • 业务cache刷新
  • 价格变化等重要业务消息

 

工作原理

 

mysql主备复制实现:

 

 

 

从上层来看,复制分成三步:

 

  • master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
  • slave将master的binary log events拷贝到它的中继日志(relay log);
  • slave重做中继日志中的事件,将改变反映它自己的数据。

 

canal的工作原理

 

 

 

原理相对比较简单:

 

  • canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  • mysql master收到dump请求,开始推送binary log给slave(也就是canal)
  • canal解析binary log对象(原始为byte流)

 

架构设计

 

个人理解,数据增量订阅与消费应当有如下几个点:

 

  1. 增量订阅和消费模块应当包括binlog日志抓取,binlog日志解析,事件分发过滤(EventSink),存储(EventStore)等主要模块。
  2. 如果需要确保HA可以采用Zookeeper保存各个子模块的状态,让整个增量订阅和消费模块实现无状态化,当然作为consumer(客户端)的状态也可以保存在zk之中。
  3. 整体上通过一个Manager System进行集中管理,分配资源。

 

可以参考下图:

 

 

 

canal架构设计

 

 

 

说明:

 

  • server代表一个canal运行实例,对应于一个jvm
  • instance对应于一个数据队列 (1个server对应1..n个instance)

 

instance模块:

 

  • eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
  • eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
  • eventStore (数据存储)
  • metaManager (增量订阅&消费信息管理器)

 

EventParser

 

 

 

整个parser过程大致可分为几部:

 

  • Connection获取上一次解析成功的位置(如果*次启动,则获取初始制定的位置或者是当前数据库的binlog位点)
  • Connection建立连接,发生BINLOG_DUMP命令
  • Mysql开始推送Binary Log
  • 接收到的Binary Log通过Binlog parser进行协议解析,补充一些特定信息
  • 传递给EventSink模块进行数据存储,是一个阻塞操作,直到存储成功
  • 存储成功后,定时记录Binary Log位置

 

EventSink设计

 

 

 

说明:

 

  • 数据过滤:支持通配符的过滤模式,表名,字段内容等
  • 数据路由/分发:解决1:n (1个parser对应多个store的模式)
  • 数据归并:解决n:1 (多个parser对应1个store)
  • 数据加工:在进入store之前进行额外的处理,比如join

 

1 数据1:n业务  :

 

为了合理的利用数据库资源, 一般常见的业务都是按照schema进行隔离,然后在mysql上层或者dao这一层面上,进行一个数据源路由,屏蔽数据库物理位置对开发的影响,阿里系主要是通过cobar/tddl来解决数据源路由问题。所以,一般一个数据库实例上,会部署多个schema,每个schema会有由1个或者多个业务方关注。

 

2 数据n:1业务:

 

同样,当一个业务的数据规模达到一定的量级后,必然会涉及到水平拆分和垂直拆分的问题,针对这些拆分的数据需要处理时,就需要链接多个store进行处理,消费的位点就会变成多份,而且数据消费的进度无法得到尽可能有序的保证。所以,在一定业务场景下,需要将拆分后的增量数据进行归并处理,比如按照时间戳/全局id进行排序归并.

 

EventStore设计

 

目前实现了Memory内存、本地file存储以及持久化到zookeeper以保障数据集群共享。

Memory内存的RingBuffer设计:

 

 

 

定义了3个cursor

 

  • Put : Sink模块进行数据存储的*后一次写入位置
  • Get : 数据订阅获取的*后一次提取位置
  • Ack : 数据消费成功的*后一次消费位置

 

借鉴Disruptor的RingBuffer的实现,将RingBuffer拉直来看:

 

 

 

实现说明:

 

  • Put/Get/Ack cursor用于递增,采用long型存储
  • buffer的get操作,通过取余或者与操作。(与操作:cusor & (size – 1) , size需要为2的指数,效率比较高)

 

Instance设计

 

 

 

instance代表了一个实际运行的数据队列,包括了EventPaser,EventSink,EventStore等组件。

 

抽象了CanalInstanceGenerator,主要是考虑配置的管理方式:

 

1. manager方式:和你自己的内部web console/manager系统进行对接。(alibaba内部使用方式)

 

2. spring方式:基于spring xml + properties进行定义,构建spring配置.

 

  • spring/memory-instance.xml 所有的组件(parser , sink , store)都选择了内存版模式,记录位点的都选择了memory模式,重启后又会回到初始位点进行解析。特点:速度*快,依赖*少
  • spring/file-instance.xml 所有的组件(parser , sink , store)都选择了基于file持久化模式,注意,不支持HA机制.支持单机持久化
  • spring/default-instance.xml 所有的组件(parser , sink , store)都选择了持久化模式,目前持久化的方式主要是写入zookeeper,保证数据集群共享. 支持HA
  • spring/group-instance.xml 主要针对需要进行多库合并时,可以将多个物理instance合并为一个逻辑instance,提供客户端访问。场景:分库业务。比如产品数据拆分了4个库,每个库会有一个instance,如果不用group,业务上要消费数据时,需要启动4个客户端,分别链接4个instance实例。使用group后,可以在canal server上合并为一个逻辑instance,只需要启动1个客户端,链接这个逻辑instance即可.

 

Server设计

 

 

server代表了一个canal的运行实例,为了方便组件化使用,特意抽象了Embeded(嵌入式) / Netty(网络访问)的两种实现:

 

  • Embeded : 对latency和可用性都有比较高的要求,自己又能hold住分布式的相关技术(比如failover)
  • Netty : 基于netty封装了一层网络协议,由canal server保证其可用性,采用的pull模型,当然latency会稍微打点折扣,不过这个也视情况而定。

 

增量订阅/消费设计

 

 

具体的协议格式,可参见:CanalProtocol.proto

get/ack/rollback协议介绍:

 

  • Message getWithoutAck(int batchSize),允许指定batchSize,一次可以获取多条,每次返回的对象为Message,包含的内容为:
  • a. batch id 唯一标识
  • b. entries 具体的数据对象,对应的数据对象格式:EntryProtocol.proto
  • void rollback(long batchId),顾命思议,回滚上次的get请求,重新获取数据。基于get获取的batchId进行提交,避免误操作
  • void ack(long batchId),顾命思议,确认已经消费成功,通知server删除数据。基于get获取的batchId进行提交,避免误操作
  • canal的get/ack/rollback协议和常规的jms协议有所不同,允许get/ack异步处理,比如可以连续调用get多次,后续异步按顺序提交ack/rollback,项目中称之为流式api.
  • 流式api设计的好处:
  • get/ack异步化,减少因ack带来的网络延迟和操作成本 (99%的状态都是处于正常状态,异常的rollback属于个别情况,没必要为个别的case牺牲整个性能)
  • get获取数据后,业务消费存在瓶颈或者需要多进程/多线程消费时,可以不停的轮询get数据,不停的往后发送任务,提高并行化. (作者在实际业务中的一个case:业务数据消费需要跨中美网络,所以一次操作基本在200ms以上,为了减少延迟,所以需要实施并行化)

 

流式api设计:

 

 

 

  • 每次get操作都会在meta中产生一个mark,mark标记会递增,保证运行过程中mark的唯一性
  • 每次的get操作,都会在上一次的mark操作记录的cursor继续往后取,如果mark不存在,则在last ack cursor继续往后取
  • 进行ack时,需要按照mark的顺序进行数序ack,不能跳跃ack. ack会删除当前的mark标记,并将对应的mark位置更新为last ack cusor
  • 一旦出现异常情况,客户端可发起rollback情况,重新置位:删除所有的mark, 清理get请求位置,下次请求会从last ack cursor继续往后取

 

数据格式

 

canal采用protobuff:

 

Entry

Header

logfileName [binlog文件名]

logfileOffset [binlog position]

executeTime [发生的变更]

schemaName

tableName

eventType [insert/update/delete类型]

entryType   [事务头BEGIN/事务尾END/数据ROWDATA]

storeValue  [byte数据,可展开,对应的类型为RowChange]

RowChange

isDdl       [是否是ddl变更操作,比如create table/drop table]

sql     [具体的ddl sql]

rowDatas    [具体insert/update/delete的变更数据,可为多条,1个binlog event事件可对应多条变更,比如批处理]

beforeColumns [Column类型的数组]

afterColumns [Column类型的数组]

Column

index

sqlType     [jdbc type]

name        [column name]

isKey       [是否为主键]

updated     [是否发生过变更]

isNull      [值是否为null]

value       [具体的内容,注意为文本]

 

canal-message example:

 

比如数据库中的表:

 

mysql> select * from person;

+—-+——+——+——+

| id | name | age  | sex  |

+—-+——+——+——+

|  1 | zzh  |   10 | m    |

|  3 | zzh3 |   12 | f    |

|  4 | zzh4 |    5 | m    |

+—-+——+——+——+

3 rows in set (0.00 sec)

 

更新一条数据(update person set age=15 where id=4):

 

****************************************************

* Batch Id: [2] ,count : [3] , memsize : [165] , Time : 2016-09-07 15:54:18

* Start : [mysql-bin.000003:6354:1473234846000(2016-09-07 15:54:06)]

* End : [mysql-bin.000003:6550:1473234846000(2016-09-07 15:54:06)]

****************************************************

 

================> binlog[mysql-bin.000003:6354] , executeTime : 1473234846000 , delay : 12225ms

BEGIN —-> Thread id: 67

—————-> binlog[mysql-bin.000003:6486] , name[canal_test,person] , eventType : UPDATE , executeTime : 1473234846000 , delay : 12225ms

id : 4    type=int(11)

name : zzh4    type=varchar(100)

age : 15    type=int(11)    update=true

sex : m    type=char(1)

—————-

END —-> transaction id: 308

================> binlog[mysql-bin.000003:6550] , executeTime : 1473234846000 , delay : 12240ms

 

HA机制设计

 

canal的HA分为两部分,canal server和canal client分别有对应的ha实现:

 

  • canal server: 为了减少对mysql dump的请求,不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态.
  • canal client: 为了保证有序性,一份instance同一时间只能由一个canal client进行get/ack/rollback操作,否则客户端接收无法保证有序。

 

整个HA机制的控制主要是依赖了zookeeper的几个特性,watcher和EPHEMERAL节点(和session生命周期绑定),可以看下我之前zookeeper的相关文章。

 

Canal Server:

 

 

 

大致步骤:

 

  • canal server要启动某个canal instance时都先向zookeeper进行一次尝试启动判断 (实现:创建EPHEMERAL节点,谁创建成功就允许谁启动)
  • 创建zookeeper节点成功后,对应的canal server就启动对应的canal instance,没有创建成功的canal instance就会处于standby状态
  • 一旦zookeeper发现canal server A创建的节点消失后,立即通知其他的canal server再次进行步骤1的操作,重新选出一个canal server启动instance.
  • canal client每次进行connect时,会首先向zookeeper询问当前是谁启动了canal instance,然后和其建立链接,一旦链接不可用,会重新尝试connect.
  • Canal Client的方式和canal server方式类似,也是利用zokeeper的抢占EPHEMERAL节点的方式进行控制.

 

HA配置架构图(举例)如下所示:

 

 

 

canal其他链接方式

 

canal还有几种连接方式:

 

1. 单连

 

 

 

2. 两个client+两个instance+1个mysql

 

当mysql变动时,两个client都能获取到变动

 

 

 

3. 一个server+两个instance+两个mysql+两个client

 

 

 

4. instance的standby配置

 

 

 

整体架构

 

从整体架构上来说canal是这种架构的(canal中没有包含一个运维的console web来对接,但要运用于分布式环境中肯定需要一个Manager来管理):

 

 

 

一个总体的manager system对应于n个Canal Server(物理上来说是一台服务器), 那么一个Canal Server对应于n个Canal Instance(destinations). 大体上是三层结构,第二层也需要Manager统筹运维管理。

 

那么随着Docker技术的兴起,是否可以试一下下面的架构呢?

 

 

 

  • 一个docker中跑一个instance服务,相当于略去server这一层的概念。
  • Manager System中配置一个instance,直接调取一个docker发布这个instance,其中包括向这个instance发送配置信息,启动instance服务.
  • instance在运行过程中,定时刷新binlog filename+ binlog position的信息至zk。
  • 如果一个instance出现故障,instance本身报错或者zk感知此node消失,则根据相应的信息,比如上一步保存的binlog filename+binlog position重新开启一个docker服务,当然这里可以适当的加一些重试机制。
  • 当要更新时,类似AB test, 先关闭一个docker,然后开启新的已更新的替换,循序渐进的进行。
  • 当涉及到分表分库时,多个物理表对应于一个逻辑表,可以将结果存于一个公共的模块(比如MQ),或者单独存取也可以,具体情况具体分析
  • 存储可以参考canal的多样化:内存,文件,zk,或者加入至MQ中
  • docker由此之外的工具管理,比如kubernetes
  • 也可以进一步添加HA的功能,两个docker对应一个mysql,互为主备,类似Canal的HA架构。如果时效性不是贴别强的场景,考虑到成本,此功能可以不采用。

 

总结

 

这里总结了一下Canal的一些点,仅供参考:

 

  1. 原理:模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议;mysql master收到dump请求,开始推送binary log给slave(也就是canal);解析binary log对象(原始为byte流)
  2. 重复消费问题:在消费端解决。
  3. 采用开源的open-replicator来解析binlog
  4. canal需要维护EventStore,可以存取在Memory, File, zk
  5. canal需要维护客户端的状态,同一时刻一个instance只能有一个消费端消费
  6. 数据传输格式:protobuff
  7. 支持binlog format 类型:statement, row, mixed. 多次附加功能只能在row下使用,比如otter
  8. binlog position可以支持保存在内存,文件,zk中
  9. instance启动方式:rpc/http; 内嵌
  10. 有ACK机制
  11. 无告警,无监控,这两个功能都需要对接外部系统
  12. 方便快速部署。

 

参考资料

 

  • https://github.com/alibaba/canal

linux系统访问NAS网络存储

测试环境:

(1)     公司内部一台NAS存储,共享存储目录share 共享,访问路径为 \\10.1.7.29

(2)Window 8测试如下图

174804_GBR8_997894.png

然后在其share共享目录创建一个测试目录aa并上传多个测试文件,E如下图所示1;2:

174957_Hgcj_997894.jpg

175146_CkhH_997894.png

Linux(Redhat5.7)系统访问NAS存储中的共享目录:

(1)     安装相关服务samba(smb)使用下面命令:

yum  install  samba*     (yum配置可用)

(2)     通过命令可以查看当前网络共享主机

Smbclient  –L  主机名(或主机ip地址) ,看到共享服务器中德共享目录

命令输入如下图:

175253_6hPY_997894.jpg

(3)     使用命令行模式查看共享目录中有那些文件,如下图

175359_Xu6H_997894.jpg

(4)     使用命令将共享服务器映射到Linux系统主机,命令如下

mount  //主机名(或服务器Ip地址)/共享目录  /挂载目录/   (备注:相当于映射网络驱动,可设置每次重启自动挂载)。

mount //10.1.7.29/share  /mnt/NASshare/

175452_uQwh_997894.png

(5)     然后就可以当相应挂载点/mnt/NASshare/目录下看到共享服务器share共享目录中的aa文件目录,点击目录就可看到目录中的文件,如下图所示:

175606_0atU_997894.jpg

如需要打开相应的办公文件知道Linux主机中有相应的应用程序即可。

Linux系统打开Pdf文档如图所示:

175706_2Tm5_997894.png

JVM 与 Linux 的内存关系详解

转载自:https://mp.weixin.qq.com/s/pSCBUqMqW1kW4iPOLq824A

在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约 600m,Linux自身使用大约800m。

从表面上,物理内存应该是足够使用的;但实际运行的情况是,会发生大量使用SWAP(说明物理内存不够使用 了),如下图所示。由于SWAP和GC同时发生会致使JVM严重卡顿,所以我们要追问:内存究竟去哪儿了?

 

 

要分析这个问题,理解JVM和操作系统之间的内存关系非常重要。接下来主要就Linux与JVM之间的内存关系进行一些分析。

一、Linux与进程内存模型

JVM以一个进程(Process)的身份运行在Linux系统上,了解Linux与进程的内存关系,是理解JVM与Linux内存的关系的基础。下图给出了硬件、系统、进程三个层面的内存之间的概要关系。

 

从硬件上看,Linux系统的内存空间由两个部分构成:物理内存和SWAP(位于磁盘)。物理内存是Linux活动时使用的主要内存区域;当物理内存不够使用时,Linux会把一部分暂时不用的内存数据放到磁盘上的SWAP中去,以便腾出更多的可用内存空间;而当需要使用位于SWAP的数据时,必须 先将其换回到内存中。

从Linux系统上看,除了引导系统的BIN区,整个内存空间主要被分成两个部分:内核内存(Kernel space)、用户内存(User space)。

内核内存是Linux自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件资源等程序逻辑使用。

用户内存是提供给各个进程主要空间,Linux给各个进程提供相同的虚拟内存空间;这使得进程之间相互独立,互不干扰。实现的方法是采用虚拟内存技术:给每一个进程一定虚拟内存空间,而只有当虚拟内存实 际被使用时,才分配物理内存。

如下图所示,对于32的Linux系统来说,一般将0~3G的虚拟内存空间分配做为用户空间,将3~4G的虚拟内存空间分配 为内核空间;64位系统的划分情况是类似的。

 

从进程的角度来看,进程能直接访问的用户内存(虚拟内存空间)被划分为5个部分:代码区、数据区、堆区、栈区、未使用区。

代码区中存放应用程序的机器代码,运行过程中代码不能被修改,具有只读和固定大小的特点。

数据区中存放了应用程序中的全局数据,静态数据和一些常量字符串等,其大小也是固定的。

堆是运行时程序动态申请的空间,属于程序运行时直接申请、释放的内存资源。

栈区用来存放函数的传入参数、临时变量,以及返回地址等数据。

未使用区是分配新内 存空间的预备区域。

二、进程与JVM内存空间

JVM本质就是一个进程,因此其内存空间(也称之为运行时数据区,注意与JMM的区别)也有进程的一般特点。深入浅出 Java 中 JVM 内存管理,这篇参考下。

但是,JVM又不是一个普通的进程,其在内存空间上有许多崭新的特点,主要原因有两 个:

1.JVM将许多本来属于操作系统管理范畴的东西,移植到了JVM内部,目的在于减少系统调用的次数;

2. Java NIO,目的在于减少用于读写IO的系统调用的开销。JVM进程与普通进程内存模型比较如下图:

 

需要说明的是,这个模型的并不是JVM内存使用的精确模型,更侧重于从操作系统的角度而省略了一些JVM的内部细节(尽管也很重要)。下面从用户内存和内核内存两个方面讲解JVM进程的内存特点。

1.用户内存

上图特别强调了JVM进程模型的代码区和数据区指的是JVM自身的,而非Java程序的。普通进程栈区,在JVM一般仅仅用做线程栈。JVM的堆区和普通进程的差别是*大的,下面具体详细说明:

首先是永久代。永久代本质上是Java程序的代码区和数据区。Java程序中类(class),会被加载到整个区域的不同数据结构中去,包括常量池、域、方法数据、方法体、构造函数、以及类中的专用方法、实例初始化、接口初始化等。这个区域对于操作系统来说,是堆的一个部分;而对于Java程序来 说,这是容纳程序本身及静态资源的空间,使得JVM能够解释执行Java程序。

其次是新生代和老年代。新生代和老年代才是Java程序真正使用的堆空间,主要用于内存对象的存储;但是其管理方式和普通进程有本质的区别。

普通进程在运行时给内存对象分配空间时,比如C++执行new操作时,会触发一次分配内存空间的系统调用,由操作系统的线程根据对象的大小分配好空间后返 回;同时,程序释放对象时,比如C++执行delete操作时,也会触发一次系统调用,通知操作系统对象所占用的空间已经可以回收。

JVM对内存的使用和一般进程不同。JVM向操作系统申请一整段内存区域(具体大小可以在JVM参数调节)作为Java程序的堆(分为新生代和老年代);当Java程序申请内存空间,比如执行new操作,JVM将在这段空间中按所需大小分配给Java程序,并且Java程序不负责通知JVM何时可以释放这 个对象的空间,垃圾对象内存空间的回收由JVM进行。

JVM的内存管理方式的优点是显而易见的,包括:*,减少系统调用的次数,JVM在给Java程序分配内存空间时不需要操作系统干预,仅仅在 Java堆大小变化时需要向操作系统申请内存或通知回收,而普通程序每次内存空间的分配回收都需要系统调用参与;第二,减少内存泄漏,普通程序没有(或者 没有及时)通知操作系统内存空间的释放是内存泄漏的重要原因之一,而由JVM统一管理,可以避免程序员带来的内存泄漏问题。

*后是未使用区,未使用区是分配新内存空间的预备区域。对于普通进程来说,这个区域被可用于堆和栈空间的申请及释放,每次堆内存分配都会使用这个区 域,因此大小变动频繁;对于JVM进程来说,调整堆大小及线程栈时会使用该区域,而堆大小一般较少调整,因此大小相对稳定。操作系统会动态调整这个区域的 大小,并且这个区域通常并没有被分配实际的物理内存,只是允许进程在这个区域申请堆或栈空间。

2.内核内存

应用程序通常不直接和内核内存打交道,内核内存由操作系统进行管理和使用;不过随着Linux对性能的关注及改进,一些新的特性使得应用程序可以使 用内核内存,或者是映射到内核空间。Java NIO正是在这种背景下诞生的,其充分利用了Linux系统的新特性,提升了Java程序的IO性能。

 

上图给出了Java NIO使用的内核内存在linux系统中的分布情况。nio buffer主要包括:nio使用各种channel时所使用的ByteBuffer、Java程序主动使用 ByteBuffer.allocateDirector申请分配的Buffer。

而在PageCache里面,nio使用的内存主要包 括:FileChannel.map方式打开文件占用mapped、FileChannel.transferTo和 FileChannel.transferFrom所需要的Cache(图中标示 nio file)。

通过JMX可以监控到NIO Buffer和 mapped 的使用情况,如下图所示。不过,FileChannel的实现是通过系统调用使用原生的PageCache,过程对于Java是透明的,无法监控到这部分内存的使用大小。

 

Linux和Java NIO在内核内存上开辟空间给程序使用,主要是减少不要的复制,以减少IO操作系统调用的开销。例如,将磁盘文件的数据发送网卡,使用普通方法和NIO时,数据流动比较下图所示:

 

将数据在内核内存和用户内存之间拷贝是比较消耗资源和时间的事情,而从上图我们可以看到,通过NIO的方式减少了2次内核内存和用户内存之间的数据拷贝。这是Java NIO高性能的重要机制之一(另一个是异步非阻塞)。

从上面可以看出,内核内存对于Java程序性能也非常重要,因此,在划分系统内存使用时候,一定要给内核留出一定可用空间。

三、案例分析

1.内存分配问题

通过上面的分析,省略比较小的区域,可以总结JVM占用的内存:
JVM内存 ≈ Java永久代 + Java堆(新生代和老年代) + 线程栈+ Java NIO

回到文章开头提出的问题,原来的内存分配是:6g(java堆) + 600m(监控) + 800m(系统),剩余大约600m内存未分配。

现在分析这600m内存的分配情况:

  1. Linux保留大约200m,这部分是Linux正常运行的需要,
  2. Java服务的线程数量是160个,JVM默认的线程栈大小是1m,因此使用160m内存,
  3. Java NIO buffer,通过JMX查到*多占用了200m,
  4. Java服务使用NIO大量读写文件,需要使用PageCache,正如前面分析,这个暂时不好定量估算大小。

前三项加起来已经560m,因此可以断定Linux物理内存不够使用。

细心的人会发现,引言中给出两个服务器,一个SWAP*多占用了2.16g,另外一个SWAP*多占用了871m;但是,似乎我们的内存缺口没有那么大。事实上,这是由于SWAP和GC同时进行造成的,从下图可以看到,SWAP的使用和长时间的GC在同一时刻发生。

 

 

SWAP和GC同时发生会导致GC时间很长,JVM严重卡顿,*端的情况下会导致服务崩溃。原因如下:JVM进行GC时,时需要对相应堆分区的已用 内存进行遍历;假如GC的时候,有堆的一部分内容被交换到SWAP中,遍历到这部分的时候就需要将其交换回内存,同时由于内存空间不足,就需要把内存中堆 的另外一部分换到SWAP中去;于是在遍历堆分区的过程中,(*端情况下)会把整个堆分区轮流往SWAP写一遍。Linux对SWAP的回收是滞后的,我 们就会看到大量SWAP占用。上述问题,可以通过减少堆大小,或者增加物理内存解决。

因此,我们得出一个结论:部署Java服务的Linux系统,在内存分配上,需要避免SWAP的使用;具体如何分配需要综合考虑不同场景下JVM对Java永久代 、Java堆(新生代和老年代)、线程栈、Java NIO所使用内存的需求。

2.内存泄漏问题

另一个案例是,8g内存的服务器,Linux使用800m,监控进程使用600m,堆大小设置4g;系统可用内存有2.5g左右,但是也发生了大量的SWAP占用。

分析这个问题如下:

  1. 在这个场景中, Java永久代 、Java堆(新生代和老年代)、线程栈所用内存基本是固定的,因此,占用内存过多的原因就定位在Java NIO上。
  2. 根据前面的模型,Java NIO使用的内存主要分布在Linux内核内存的System区和PageCache区。查看监控的记录,如下图,我们可以看到发生SWAP之前,也就是 物理内存不够使用的时候,PageCache急剧缩小。因此,可以定位在System区的Java NIO Buffer发生内存泄漏。

     

     

     

  3. 由于NIO的DirectByteBuffer需要在GC的后期被回收,因此连续申请DirectByteBuffer的程序,通常需要调用 System.gc(),避免长时间不发生FullGC导致引用在old区的DirectByteBuffer内存泄漏。分析到此,可以推断有两种可能的 原因:*,Java程序没有在必要的时候调用System.gc();第二,System.gc()被禁用。
  4. *后是要排查JVM启动参数和Java程序的DirectByteBuffer使用情况。在本例中,查看JVM启动参数,发现启用了-XX:+DisableExplicitGC导致System.gc()被禁用。

四、总结

本文详细分析了Linux与JVM的内存关系,比较了一般进程与JVM进程使用内存的异同点,理解这些特性将对Linux系统内存分配、JVM调优、Java程序优化有帮助。限于篇幅关系仅仅列举两个案例,希望起到抛砖引玉的作用。

Linux 如何挂载NAS盘

linux下需要将nas盘挂在到系统中;
方法:
首先创建一个挂载目录:

    mkdir /mnt/nas

 

挂载目录:

    mount  -o username=flt,password=a^6r9SDy,iocharset=utf8 //192.168.2.90/产品部 /mnt/nas

 

如果是想要mount的是windows文件系统,需要加上cifs:

    mount -t cifs -o username=flt,password=a^6r9SDy,iocharset=utf8 //192.168.2.90/产品部 /mnt/nas

 

参数说明:

        username=flt                        [nas用户名]
        password=a^6r9dSDy                  [nas密码]     
        iocharset=utf8                      [路径中如有中文则添加此项,支持中文路径] 
        //192.168.2.90/产品部                [nas路径]
        /mnt/nas                            [挂载路径]

参数之间用,(逗号)分隔。

如果密码中有’,’号,则以上命令无法执行完成,因为需要参数之间用“,”逗号分隔,导致会认为提前结束了。
所以可以用下面方法,创建一个环境变量:

export PASSWD='!3\5g6,B'

mount -o username=mkx,iocharset=utf8 //192.168.10.90/产品部 /mnt/nas

注:shell会自动查找PASSWD

使用完成后将其卸载:

umount -l /mnt/nas

u盘无法挂载问题

不正常拔出U盘后,下次插入可能出现

Error mounting /dev/sdc1 at /media/my/XXX: Command-line `mount -t “ntfs” -o……

terminal下,通过以下指令解决:

sudo apt-get install ntfs-3g
sudo ntfsfix /dev/sdc1

/dev/sdc1/ 对应mounting后的位置

老大难的 Java ClassLoader,到了该彻底理解它的时候了

转载自:http://blog.itpub.net/31561269/viewspace-2222522/

ClassLoader 是 Java 届*为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里。网上的文章也是一篇又一篇,经过本人的亲自鉴定,*大部分内容都是在误导别人。本文我带读者彻底吃透 ClassLoader,以后其它的相关文章你们可以不必再细看了。

ClassLoader 做什么的?

顾名思义,它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

 

 

有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

  1. class Class<T> {
  2.   …
  3.   private final ClassLoader classLoader;
  4.   …
  5. }

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

各司其职

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的服务地址来加载。

JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。

BootstrapClassLoader 负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。

ExtensionClassLoader 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这*个用户类的加载器就是 AppClassLoader。

ClassLoader 传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

双亲委派

前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。

 

 

AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。

而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。

这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。

  1. class ClassLoader {
  2.   …
  3.   private final ClassLoader parent;
  4.   …
  5. }

值得注意的是图中的 ExtensionClassLoader 的 parent 指针画了虚线,这是因为它的 parent 的值是 null,当 parent 字段是 null 时就表示它的父加载器是「根加载器」。如果某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。

Class.forName

当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

  1. class Driver {
  2.   static {
  3.     try {
  4.        java.sql.DriverManager.registerDriver(new Driver());
  5.     } catch (SQLException E) {
  6.        throw new RuntimeException(“Can’t register driver!”);
  7.     }
  8.   }
  9.   …
  10. }

forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载

Class<?> forName(String name, boolean initialize, ClassLoader cl)

通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。

自定义加载器

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程

  1. class ClassLoader {
  2.   // 加载入口,定义了双亲委派规则
  3.   Class loadClass(String name) {
  4.     // 是否已经加载了
  5.     Class t = this.findFromLoaded(name);
  6.     if(t == null) {
  7.       // 交给双亲
  8.       t = this.parent.loadClass(name)
  9.     }
  10.     if(t == null) {
  11.       // 双亲都不行,只能靠自己了
  12.       t = this.findClass(name);
  13.     }
  14.     return t;
  15.   }
  16.   // 交给子类自己去实现
  17.   Class findClass(String name) {
  18.     throw ClassNotFoundException();
  19.   }
  20.   // 组装Class对象
  21.   Class defineClass(byte[] code, String name) {
  22.     return buildClassFromCode(code, name);
  23.   }
  24. }
  25. class CustomClassLoader extends ClassLoader {
  26.   Class findClass(String name) {
  27.     // 寻找字节码
  28.     byte[] code = findCodeFromSomewhere(name);
  29.     // 组装Class对象
  30.     return this.defineClass(code, name);
  31.   }
  32. }

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

  1. // ClassLoader 构造器
  2. protected ClassLoader(String name, ClassLoader parent);

双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。

Class.forName vs ClassLoader.loadClass

这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。

  1. Class<?> x = Class.forName(“[I”);
  2. System.out.println(x);
  3. x = ClassLoader.getSystemClassLoader().loadClass(“[I”);
  4. System.out.println(x);
  5. ———————
  6. class [I
  7. Exception in thread “main” java.lang.ClassNotFoundException: [I

钻石依赖

项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。

 

 

我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。

 

使用 ClassLoader 可以解决钻石依赖问题。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同 ClassLoader 中名称一样的类实际上是不同的类。下面让我们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader

  1. $ cat ~/source/jcl/v1/Dep.java
  2. public class Dep {
  3.     public void print() {
  4.         System.out.println(“v1”);
  5.     }
  6. }
  7. $ cat ~/source/jcl/v2/Dep.java
  8. public class Dep {
  9.   public void print() {
  10.     System.out.println(“v1”);
  11.   }
  12. }
  13. $ cat ~/source/jcl/Test.java
  14. public class Test {
  15.     public static void main(String[] args) throws Exception {
  16.         String v1dir = “file:///Users/qianwp/source/jcl/v1/”;
  17.         String v2dir = “file:///Users/qianwp/source/jcl/v2/”;
  18.         URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
  19.         URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});
  20.         Class<?> depv1Class = v1.loadClass(“Dep”);
  21.         Object depv1 = depv1Class.getConstructor().newInstance();
  22.         depv1Class.getMethod(“print”).invoke(depv1);
  23.         Class<?> depv2Class = v2.loadClass(“Dep”);
  24.         Object depv2 = depv2Class.getConstructor().newInstance();
  25.         depv2Class.getMethod(“print”).invoke(depv2);
  26.         System.out.println(depv1Class.equals(depv2Class));
  27.    }
  28. }

在运行之前,我们需要对依赖的类库进行编译

  1. $ cd ~/source/jcl/v1
  2. $ javac Dep.java
  3. $ cd ~/source/jcl/v2
  4. $ javac Dep.java
  5. $ cd ~/source/jcl
  6. $ javac Test.java
  7. $ java Test
  8. v1
  9. v2
  10. false

在这个例子中如果两个 URLClassLoader 指向的路径是一样的,下面这个表达式还是 false,因为即使是同样的字节码用不同的 ClassLoader 加载出来的类都不能算同一个类

depv1Class.equals(depv2Class)

我们还可以让两个不同版本的 Dep 类实现同一个接口,这样可以避免使用反射的方式来调用 Dep 类里面的方法。

  1. Class<?> depv1Class = v1.loadClass(“Dep”);
  2. IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
  3. depv1.print()

ClassLoader 固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven 没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的 ClassLoader,那么从头到尾都是在使用 AppClassLoader,而不同版本的同名类必须使用不同的 ClassLoader 加载,所以 Maven 不能完美解决钻石依赖。

如果你想知道有没有开源的包管理工具可以解决钻石依赖的,我推荐你了解一下 sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。

分工与合作

这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

 

 

不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。

Thread.contextClassLoader

如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别

  1. class Thread {
  2.   …
  3.   private ClassLoader contextClassLoader;
  4.   public ClassLoader getContextClassLoader() {
  5.     return contextClassLoader;
  6.   }
  7.   public void setContextClassLoader(ClassLoader cl) {
  8.     this.contextClassLoader = cl;
  9.   }
  10.   …
  11. }

contextClassLoader「线程上下文类加载器」,这究竟是什么东西?

首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它

Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。

它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

 

线程的 contextClassLoader 使用场合比较罕见,如果上面的逻辑晦涩难懂也不必过于计较。

JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。

NFS服务器不能挂载问题终*解决办法

大部分NFS服务器不能挂载的问题都是windows端的NFS服务出现了问题,现在环境如下,win7下的NFS服务,虚拟机Ubuntu和开发板liunx,同时使用NFS服务进行挂载,开始使用正常,突然有一天不能使用了,挂载不上了,应该就是win7下的NFS服务出现了问题。无论出现什么问题,可以用以下两种方法解决:

1. 单击win按钮,找到haneWIN软件,或者到该软件的安装目录,默认在C:\Program Files (x86)\nfsd 目录下,右键以管理员身份重启所有服务。

咱们从头到尾说一次 Java 垃圾回收

转载自:https://mp.weixin.qq.com/s/7d1-xEvMKSav1Gcmla2u_g

⬆️ 图片来源于网络

 

之前上学的时候有这个一个梗,说在食堂里吃饭,吃完把餐盘端走清理的,是 C++ 程序员,吃完直接就走的,是 Java 程序员。?

 

确实,在 Java 的世界里,似乎我们不用对垃圾回收那么的专注,很多初学者不懂 GC,也依然能写出一个能用甚至还不错的程序或系统。但其实这并不代表 Java 的 GC 就不重要。相反,它是那么的重要和复杂,以至于出了问题,那些初学者除了打开 GC 日志,看着一堆0101的天文,啥也做不了。?

 

今天我们就从头到尾完整地聊一聊 Java 的垃圾回收。
 

什么是垃圾回收


 

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

 

Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?

 

1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史!

 

怎么定义垃圾


 

既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。

 

 

引用计数算法

引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。

String m = new String("jack");

 

先创建一个字符串,这时候”jack”有一个引用,就是 m。

 

然后将 m 设置为 null,这时候”jack”的引用次数就等于0了,在引用计数算法中,意味着这块内容就需要被回收了。

m = null;

 

 

引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的”Stop-The-World”的垃圾收集机制。

 

看似很美好,但我们知道JVM的垃圾回收就是”Stop-The-World”的,那是什么原因导致我们*终放弃了引用计数算法呢?看下面的例子。

 

  1. public class ReferenceCountingGC {
  2. public Object instance;
  3. public ReferenceCountingGC(String name){}
  4. }
  5. public static void testGC(){
  6. ReferenceCountingGC a = new ReferenceCountingGC(“objA”);
  7. ReferenceCountingGC b = new ReferenceCountingGC(“objB”);
  8. a.instance = b;
  9. b.instance = a;
  10. a = null;
  11. b = null;
  12. }

 

1. 定义2个对象

2. 相互引用
3. 置空各自的声明引用

我们可以看到,*后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

 

可达性分析算法

可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。

 

 

 

通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。

 

Java 内存区域

在 Java 语言中,可作为 GC Root 的对象包括以下4种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

 

 

虚拟机栈(栈帧中的本地变量表)中引用的对象
此时的 s,即为 GC Root,当s置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。

  1. public class StackLocalParameter {
  2. public StackLocalParameter(String name){}
  3. }
  4. public static void testGC(){
  5. StackLocalParameter s = new StackLocalParameter(“localParameter”);
  6. s = null;
  7. }

 

方法区中类静态属性引用的对象
s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 properties 对象由于无法与 GC Root 建立关系被回收。

而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。

  1. public class MethodAreaStaicProperties {
  2. public static MethodAreaStaicProperties m;
  3. public MethodAreaStaicProperties(String name){}
  4. }
  5. public static void testGC(){
  6. MethodAreaStaicProperties s = new MethodAreaStaicProperties(“properties”);
  7. s.m = new MethodAreaStaicProperties(“parameter”);
  8. s = null;
  9. }

 

方法区中常量引用的对象
m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。

  1. public class MethodAreaStaicProperties {
  2. public static final MethodAreaStaicProperties m = MethodAreaStaicProperties(“final”);
  3. public MethodAreaStaicProperties(String name){}
  4. }
  5. public static void testGC(){
  6. MethodAreaStaicProperties s = new MethodAreaStaicProperties(“staticProperties”);
  7. s = null;
  8. }

 

本地方法栈中引用的对象
任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

 

 

 

怎么回收垃圾


 

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法的核心思想。

标记 — 清除算法

标记清除算法(Mark-Sweep)是*基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。

这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。

上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个 2M的内存区域,其中有2个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。

复制算法

 

复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。

 

上面的图很清楚,也很明显的暴露了另一个问题,合着我这140平的大三房,只能当70平米的小两房来使?代价实在太高。

 

标记整理算法

 

标记整理算法(Mark-Compact)标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

 

标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

 

分代收集算法分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用*适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 — 整理算法来进行回收。so,另一个问题来了,那内存区域到底被分为哪几块,每一块又有什么特别适合什么算法呢?

 

 

内存模型与回收策略


 

 

 

Java 堆(Java Heap)是JVM所管理的内存中*大的一块,堆又是垃圾收集器管理的主要区域,这里我们主要分析一下 Java 堆的结构。

 

Java 堆主要分为2个区域-年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。可能这时候大家会有疑问,为什么需要 Survivor 区,为什么Survivor 还要分2个区。不着急,我们从头到尾,看看对象到底是怎么来的,而它又是怎么没的。

 

Eden 区

IBM 公司的专业研究表明,有将近98%的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。

通过 Minor GC 之后,Eden 会被清空,Eden 区中*大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。

 

Survivor 区

Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。

 

为啥需要?

不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。

 

所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

 

为啥需要俩?

设置两个 Survivor 区*大的好处就是解决内存碎片化。

 

我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除*大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

 

这种机制*大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的*佳方案。

 

Old 区

老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法。

 

除了上述所说,在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代。

 

大对象

大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。

 

长期存活对象

虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加1岁。当年龄增加到15岁时,这时候就会被转移到老年代。当然,这里的15,JVM 也支持进行特殊设置。

 

动态对象年龄

虚拟机并不重视要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总合大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。

 

这其实有点类似于负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求。看似很均衡,但每台机的硬件不通,健康状况不同,我们还可以基于每台机接受的请求数,或每台机的响应时间等,来调整我们的负载均衡算法。

 

本文部分内容参考自书籍:《深入理解Java虚拟机》。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速