之前我的两篇关于Druid连接池的文章讨论了一些关于连接保活和超时设置的问题,后来我又重新梳理了一Druid关于空闲连接检测以及KeepAlive
执行的过程,本文其实已经写了很久了,当时是基于1.2.4
版本,一直忘了发布上来。目前最新版是1.2.8
版本,建议升级到最新版本,因为1.2.4
版本存在一些连接检测异常被错误丢弃和KeepAlive
的一些Bug,不过本文探讨的大致流程没有变化,因此还是基于1.2.4
版本说明,但是请关注新版本的Releases Note。
这是一份简化的Druid配置
1 | ...... |
在当前Druid 1.2.4版本,DestroyTask
线程会按照time-between-eviction-runs-millis
时间间隔检测空闲连接,当idleMillis
(连接空闲时间=当前系统时间-lastActiveTimeMillis
)> min-evictable-idle-time-millis
,会驱逐多余超过min-idle
数量的连接,直到idleMillis
> max-evictable-idle-time-millis
,min-idle
的连接也会被关闭重新建立。如果开启keep-alive
,当idleMillis
> keep-alive-between-time-millis
,会对连接进行心跳保活,首先会执行连接检测,不同数据源的检测方式不同,MySQL连接检测有两种方式ping
和validation-query
,默认使用ping
,只检测连接有效,不会刷新相关时间参数,检测之后刷新lastKeepTimeMillis
。检测的超时时间都是取自validation-query-timeout
,默认是1。
1 |
|
当开启了test-while-idle
,获取连接后会检测空闲连接,空闲的判断逻辑大致为取lastActiveTimeMillis
和lastKeepTimeMillis
的最大值和当前系统时间对比,如果超过了time-between-eviction-runs-millis
,就认为连接空闲,需要检测,检测的第一步和上面类似,默认使用ping
,成功之后再次判断连接的空闲时间,此处是通过反射获取MySQL连接的lastPacketReceivedTimeMs
,ping
不会刷新这个时间,如果当前时间-lastPacketReceivedTimeMs
>time-between-eviction-runs-millis
,则会认为连接已经超过空闲时间,于是抛弃这个连接,打印WARN日志discard long time none received connection
.
1 | if (testWhileIdle) { |
根据当前的配置,每60s执行一次空闲检测,但是只有空闲超过120s才会执行keepalive
,所以超过空闲超过60s的连接不做处理,如果此时获取该连接,空闲连接检测生效,就会丢掉该连接.如果将最小keepalive
时间改为和空闲检测一致,每次空闲检测都会刷新lastKeepTimeMillis
,这样再获取连接不会进行空闲检测.但是这种是理想情况,如果0s进行了keepalive,间隔20s之后,执行数据库操作,当60s时,空闲为40s,无需keepalive
,当100s时,此时空闲时间为80s,如果此时获取连接,则又会进行连接空闲检测抛弃连接.(1.2.6
版本已经要求keepAliveBetweenTimeMillis
必须要大于timeBetweenEvictionRunsMillis
)
Druid检测MySQL连接的方式是根据一个系统属性druid.mysql.usePingMethod
,没有设置的情况如果有ping method
下会使用MySQL ping
进行连接检测
1 | public MySqlValidConnectionChecker(){ |
如果不想出现空闲连接被强制关闭并且出现这个Warn日志,也很好解决
只需要将druid.mysql.usePingMethod
设置为false
,这样每次连接检测都会执行validation-query
语句,因此不会再丢弃空闲连接,由于该配置为系统属性,可以通过启动参数-Ddruid.mysql.usePingMethod=false
或者代码配置
1 |
|
最近遇到了一个哭笑不得的事情,生产上面一个日期207x年变成了197x年,少了100年,排查下来原因也是让人大跌眼镜,某位同学使用了SimpleDateForma
类将一个两位数年的日期格式’yy/MM/dd’转换为Date类型,然后再转成’yyyy-MM-dd’字符串,而就是这个转换过程中丢掉了100年。
首先感觉比较奇怪的是这个场景肯定测试过,这么显眼的问题不可能没有发现,那么是否与年份有关系,于是试了一些两位数的日期年份,发现超过当前年20年之后就会变成19xx年,少了100年,而20年之内就是正常的20xx年,那么肯定是 SimpleDateFormat
类parse ‘yy’的过程中有相关设置,于是就去翻相关代码,发现了这样的逻辑:
SimpleDateFormat
类initialize
的时候会执行initializeDefaultCentury()
方法,方法源码如下:
1 | /* Initialize the fields we use to disambiguate ambiguous years. Separate |
可以看到,为了消除两位数的年的时间模糊,会去定义一个默认的世纪开始年份,默认值为当前年份向前80年,然后当执行parse
方法时,会调用subParse
方法,源码大致如下,只保留了一下相关逻辑:
1 | /** |
如果’yy’的值比当前年份减去80年的defaultCenturyStartYear
后两位小,那么取defaultCenturyStartYear
两位补齐’yy’,并且再加上100年,否则直接补齐不加100,那么对于’71’,当前defaultCenturyStartYear
为1941,71大于41,所以最终就变成了1971,所以问题原因就在于defaultCenturyStartYear
这个值默认是当前年份减去80的年,当然这个值也能修改
1 | /** |
所以,当解析两位年份的时候,SimpleDateFormat
的parse
方法会自动补齐前两位,补齐的规则是先初始化一个世纪开始年份,默认是当前日期减去80年的年份,然后补齐的年份会处于这个世纪开始年份的100年内,不能超过,因此就出现了超过当前20年的两位年份被补齐成过去的日期,少了100年,当然这个世纪开始年份也可以进行修改,设置成当前世纪的开始年份,这样日期都会补齐为当前世纪。
最近的一个业务场景中需要在内存中换成一些数据,并且需要根据时间戳有序排列,因此使用了TreeSet,但是在使用过程中确出现了IllegalArgumentException和ConcurrentModificationException,因此记录一下这两个问题.
首先是
1 | java.lang.IllegalArgumentException: fromKey > toKey |
我们的一个业务场景是需要对内存中的一些数据根据指定区间筛选出对应数据排列好之后返回给前端,因此我们选用了有序集合TreeSet,当前端传入一个区间范围[A,B]之后,可以使用E ceiling(E e)
返回大于等于A的最小元素,使用E floor(E e)
返回小于等于B的最大元素,这样就可以使用subSet就可以返回指定区间的set集合
1 | NavigableSet<E> subSet(E fromElement, boolean fromInclusive, |
这样看来,这个思路并没有什么问题.但是代码部署之后,测试环境缺偶尔抛出了java.lang.IllegalArgumentException: fromKey > toKey
异常,简而言之就是fromElement大于toElement,无法返回指定范围的set,由于入参的时候已经对于A、B大小已经进行了校验,那么只能是使用ceiling和floor方法导致返回的元素出现了问题,查看了原数据和传入的区间参数之后发现了这样一个问题,例如TreeSet存储以下元素
1 | [1,3,7,8,9] |
而传入的区间为[4,6],ceiling(4)
返回7,而floor(6)
返回3,那么subSet(7,3)
会抛出IllegalArgumentException也就不奇怪了,因此,当使用subSet方法时一定要确保fromElement小于toElement,加上这个二次校验之后,这个问题就再也没出现过了.但是过了不久之后又出现了另外一个问题
1 | java.util.ConcurrentModificationException |
这个异常也很清晰,由于并发安全问题导致的,多线程同时修改或者同时读取和修改都会导致这个问题.
我们知道,iterator遍历元素时通过源列表直接删除元素会导致ConcurrentModificationException,必须使用iterator的remove方法才能安全删除元素,使用Iterator会返回集合自身的一个迭代器,这个机制与这两个字段有关
私有
,初始时和modCount相等当进行遍历/删除时都会判断modCount和expectedModCount是否相等,不等就会抛出ConcurrentModificationException,而只有使用迭代器的remove方法才会更新expectedModCount值确保二者相等.以下相关源码
1 | final Entry<K,V> nextEntry() { |
所以必须要求Iterator迭代过程中必须使用Iterator的remove方法,但是这仅限于单线程,当多线程情况下,即使使用Iterator的remove方法仍然会有线程安全问题,因为迭代器是线程私有的,所以expectedModCount也是线程私有的,而modCount是线程共享的.如果有一个线程对集合进行了修改,那么modCount和此线程的expectedModCount会更新,但是其他线程的expectedModCount都不会更新,expectedModCount!=modCount,最终抛出ConcurrentModificationException.
我们正是犯了了这个问题,多线程同时读取和修改,导致产生了线程安全问题.所以需要将TreeSet换成线程安全的有序Set集合SynchronizedSortedSet.
java.util 包中的集合类都返回 fail-fast 迭代器,具有强一致性,当迭代器检测到迭代过程中元素进行了更改,就会抛出ConcurrentModificationException.而java.util.concurrent包中的集合类返回的是weakly consistent迭代器,即弱一致迭代器,当迭代开始,如果元素在迭代到达前被删除或者修改,这些更改会返回给调用者,但是对于插入元素则无法保证,并且不会抛出ConcurrentModificationException.
]]>临近PS5发售,索尼对PlayStation Store网页版和PS App都进行了大的更新,包含UI和功能,然而我已经大半年几乎无法正常登录网页版ps商店了,无论是win10的Chrome还是安卓的Chrome,每次登录都是提示无法连接到服务器,然后一串长长的(18.xxxx.xxxx.xxx)错误码,安卓版App由于登录也是调起网页进行登录,因此遇到同样的问题,然而我Mac的Chrome却一切正常,甚至win10和安卓的其他浏览器也是ok的,只有Chrome和PS的那几个App使用WebView让人崩溃。。。
这次更新之后,我试了下问题依旧,甚至PS App连登录页面都进不去,直接提示无法登录错误,而且似乎有不少人都遇到这个问题,于是尝试解决这俩问题
先说一下,这个问题与cookie有关系,具体确切原因我也没有确定,但是能够解决问题
只有chrome无法登录而其他浏览器正常,于是我尝试了基于Chromium的最新版Edge,结果也是正常的,于是打开控制台,对比二者的差别,于是发现了Chrome在登录的时候会有一个请求被403了,请求链接是ca.account.sony.com/api/v1/ssocookie
看样子似乎和cookie有什么关系,网上搜索了一下,也有其他人不同的浏览器遇到了同样问题,有人这样解释
Reproduced on older Chromium v74 while works in newer browser version. I’ve send request to Galaxy to update their inner browser version. It may help but not for sure.
Problem is because we’re rejected with 403 while requesting auth cookie
https://auth.api.sonyentertainmentnetwork.com/2.0/ssocookie
Akamai server blocks requests from older browser for some reason maybe because of SameSiteCookie policy, or CORS, or maybe because Akamai’s anti-bot script does not like Galaxy browser.
Login works when user requests are handled by direct PSN server (nginx header)
Login does not work when requests are handled by Akamai’s load balancer.
This is why it happen sometimes, not always.Known workarounds:
use VPN
wait some time (like a day) and try to login again when there is smaller traffic.
似乎并不能解决我的问题,我的Chrome是最新版,并且网络也是ok的,也看到了另外一个描述,Chrome 80版本之后cookie 的 SameSite 属性默认值由 None 变为 Lax,造成了一些访问跨域 cookie 无法携带的问题,尝试修改了Chrome相关设置,依然无效,这时候一条微博引起我的注意
于是进行了尝试,EditThisCookie插件拷贝了一下Edge登录页面的cookie值复制到Chrome登录页面上(直接拷贝Edge登录成功的cookie应该也是ok的,下图为截取的登录进去的cookie),于是再次登录,果然这次终于成功了😭️
问题已经知道了,PC可以很方便的修改cookie,但是安卓端并没有方便的浏览器修改cookie插件,怎么办?还是有方法的,安卓Chrome的cookie是写到本地目录里面,并且是sqlite数据库文件,存储路径为/data/data/com.android.chrome/app_chrome/Defaule/Cookie,我们可以用SQLite编辑器打开它进行修改,于是先要找到一个正常的cookie,Edge,决定就是你了,下载登录,啊咧
这尼玛,算了,咱换一个,换上学生时代经常折腾而现在许久没有使用的Firefox吧,嗯,这次正常登录了,Firefox的cookie路径是/data/data/org.mozilla.firefox/files/mozilla/xxxx.default/cookies.sqlite,直接标明是个sqlite文件,中间是路径xxxx是随机字符串,打开之后同样将psn登录所需的cookie内容拷贝复制到Chrome的cookie文件里,这里强烈建议在PC端就行操作,手机端太麻烦了,我开了俩sqlite应用,一个查看,一个修改,手忙脚乱,弄完之后才想起了为啥我不用PC搞这玩意😓️,弄好之后,把Chrome停止掉,重新打开,登录,OK,大功告成!
先说一下新版PS App登录需要的条件
不然就会出现如下错误
我新旧两版App都是无法登录,只是问题不太一样,旧版问题其实和Chrome问题一样,App通过WebView调起登录页面,而WebView是Chrome实现的,但是cookie文件存储在App自己的安装目录,而新版打开直接提示无法登录退出,推测这个可能和我Root了有关,尝试Magisk Hide和随机包名并未解决,于是暂时放弃,先搞定旧版App。
按照同样的套路,找到PS App的Cookie文件/data/data/com.scee.psxandroid/app_webview/Default/Cookie,同样进行修改,但是这次却没有成功。于是我换了个思路,PC端使用安卓模拟器成功登录App,拷贝出cookie,顺带一并拷贝登录用户数据库文件/data/data/com.scee.psxandroid/databases/signin_user.db,覆盖之后,停止应用,重新打开,由于读取了登录用户数据,不需要登录就直接进去了,但是旧的App就快要停用了,能登录似乎也不长久,于是继续搜索新版App的问题。
最终在P9看到网友回复,新版PS App初次打开需要检测SafetyNet,我擦,Sony你在搞啥,忒缺德了吧
好吧,解决掉这个试试,SafetyNet检测各个手机状况不同,可以自行查询,我是临时禁用了所有Magisk模块重启之后就OK了,然后重新下载新版PS App,终于弹出了熟悉的登录页面,而当我又以为会再次无法连接到服务器需要修改cookie的时候,这次竟然直接登录上了🙂️,好吧,省事了,废了这么大劲终于搞定了,这时候可以再把Magisk模块启用,因为登录上就不会再检测SafetyNet了。
]]>之前的上一篇文章 keepAlive解决druid空闲连接socket timeout 15分钟解决了防火墙导致的空闲连接socket timeout的问题,而这一次在另外一个没有防火墙策略的内部环境却又出现了另外一个情况,进程偶发数据库操作报错,并且个别进程一段时间后始终无法获取数据库连接一直处于宕机状态。
通过对错误日志分析,发现前期数据库报错的日志四个进程出现的时间点基本一致,因此怀疑是外部因素网络或者数据库导致。部分进程维持着偶发报错的情况下工作,而有的进程则在一段时间后彻底无法工作,一直出现无法创建数据库连接。于是首先对无法工作的进程分析排查。
首先,查看进程与数据库连接状态,发现存在ESTABLISHED
状态连接
1 | netstat -anp|grep 1521 |
而错误日志显示1
2
3
4
5
6org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 0, maxActive 20, creating 1
.......
Caused by: java.sql.SQLRecoverableException: IO Error: Connection reset
at oracle.jdbc.driver.T4CConnection.logon(T4CConnection.java:498)
at oracle.jdbc.driver.PhysicalConnection.<init>(PhysicalConnection.java:553)
可以看到当前数据库连接池中没有可用连接,druid在创建新连接的时候出现异常。
查看druid创建连接相关源码
1 | if (maxWait > 0) { |
1 | private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException { |
可以知道druid的连接获取是通过notEmpty
和empty
两个变量协调线程的同步,执行pollLast
方法发现没可用连接时,就会notEmpty.awaitNanos()
,同时empty.signal()
去唤醒CreateConnectionThread
这个线程去创建连接。
因此,查看当前进程的堆栈信息,找到CreateConnectionThread
线程。
发现如下
1 | "Druid-ConnectionPool-Create-523528914" #83 daemon prio=5 os_prio=0 tid=0x00007f6d1d8d4800 nid=0x26fb runnable [0x00007f6db3ffd000] |
而当前工作线程
1 | "http-nio-8212-exec-7" #72 daemon prio=5 os_prio=0 tid=0x00007f6d46caa800 nid=0x26ea waiting on condition [0x00007f6db90ea000] |
可以看到,工作线程执行pollLast
方法去唤醒Druid-ConnectionPool-Create
线程,Druid-ConnectionPool-Create
线程开始尝试连接数据库,但是线程一直出现了 socketRead0阻塞,导致无法创建连接,因此工作线程会一直waiting直到获取连接超时报错。而 Druid-ConnectionPool-Create
线程通过jdbc连接数据库是使用Socket通信的,Socket没有办法探测到网络错误,因此应用也无法主动发现连接错误,它的超时是由Socket Timeout控制的,如果没有设置Socket Timeout在没有返回的情况下会一直等待下去,所以当数据库或者网络突然出现故障,就可能会发生socket阻塞,而如果没有socket timeout设置,那么阻塞将一直持续下去。这样始终无法创建连接,所以进程不可用。(一般来说Linux服务器会有系统级别的socket timeout,由于没有权限查看不了,从上面信息推测设置的可能较长)
那么为什么有的进程只是偶发故障,而后恢复呢?通过日志查看,发现这种情况的情况在报错的时候,连接池当前还存在着一个连接,即使连接池创建新的连接报错,但是并不会影响该连接,所以网络恢复后,该连接仍然可用,只是无法再创建新的连接而已,所以进程体现在偶发报错,但是仍然可用。而当这个连接空闲很长时间或者因为其他原因死亡后,那么连接池将再无连接可用,也无法创建新的连接,进程变成了不可用状态。
以上就是基于日志分析、进程堆栈和连接信息排查做出的推测。
基于上面推测,问题主要出现在socket阻塞,因此解决方案就是尽量避免socket阻塞过长时间,导致长时间不可用。
如果发生阻塞,只需要重启进程即可临时解决问题,如果不想要进程重启,那么我们可以通过杀掉阻塞的socket来重连数据库。
首先通过lsof
命令找到进程的所有文件描述符,并且找到阻塞的socket的连接,然后gdb
连接进程,call close
掉这个socket连接,这样Druid-ConnectionPool-Create
线程可以再重新创建连接。
长久的解决思路肯定是设置一个合适的socket timeout来避免socket阻塞,一般会有系统环境相关设置,为了预防系统socket timeout过长或者没有设置,还是有必要设置一个jdbc级别的timeout。
上一篇文章中是通过开启keepAlive
来解决问题的,其中我也提到不建议通过修改 socket timeout
去解决。而这次的这个问题就必须要通过设置一个合理的 socket timeout
参数来保证当出现异常网路情况下服务不会宕机太久,但是这个时间又不能过小,否则会导致一些耗时较长的数据库操作被超时终止,配置参考如下。
oracle:
1 | connectionProperties: oracle.net.CONNECT_TIMEOUT=60000;oracle.jdbc.ReadTimeout=300000 |
mysql:
1 | url: jdbc:mysql://ip:port/db?connectTimeout=60000&socketTimeout=300000 |
线上的一个数据接收服务最近数据量比较大,日志也打的很频繁,但是却出现了totalSizeCap配置不生效,无法删除日志文件的问题,每天日志一度回滚累计到几千个 。
日志设置maxHistory 30,maxFileSize 20MB,totalSizeCap 5GB
1 | <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
面向Google查询相关问题,看到一个logback无法处理totalSizeCap 超过2GB的bug
1 | void capTotalSize(Date now) { |
这段代码定义了totalSize来累加当前日志文件大小,但是数据类型是int,我们知道int类型最大值是 2147483647,而如果totalSize超过了2GB totalSize + size > totalSizeCap
会不成立。。。。
所以一旦totalSizeCap超过2GB,那么就会导致日志清除失效。
这个问题有人反馈过 issue,而logback也在1.2.0版本修复了这个问题,解决方案是修改int为long类型。
但是这个并不适用于线上遇到的情况,因为服务中使用的是1.2.3版本,于是继续查询分析。又发现了一个类似报告,而且报告的版本也是1.2.3
I’m using SizeAndTimeBasedRollingPolicy.
When ‘%i’ file index reaches 999 it stops deleting the old files and totalSizeCap is not respected any more.
This soon leads to disk full issues (as logging in my case was fast enough)
这个问题有点相似了,看一下生产环境的日志文件,前几天都是只保留了后缀1000以上的,当天的保留了700以上的,也就是删除了一部分,后面未删除,而且感觉和这个999又很大关系,这个issue里面没有相关回复,那我们自己一边查看源码一般继续Google吧,上面的源码我们已经看到是日志大小回滚的实现,File[] matchingFileArray = getFilesInPeriod(date)
,这一步应该是获取相关的文件数组,我们进去继续查看
1 | protected File[] getFilesInPeriod(Date dateOfPeriodToClean) { |
上面这一块的逻辑是生成一个正则去和日志目录下的日志匹配获取日志文件,下面的代码就是具体拼接正则的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* Given date, convert this instance to a regular expression.
*
* Used to compute sub-regex when the pattern has both %d and %i, and the
* date is known.
*
* @param date - known date
*/
public String toRegexForFixedDate(Date date) {
StringBuilder buf = new StringBuilder();
Converter<Object> p = headTokenConverter;
while (p != null) {
if (p instanceof LiteralConverter) {
buf.append(p.convert(null));
} else if (p instanceof IntegerTokenConverter) {
buf.append("(\\d{1,3})");
} else if (p instanceof DateTokenConverter) {
buf.append(p.convert(date));
}
p = p.getNext();
}
return buf.toString();
}
看到这一块果然发现一个问题buf.append("(\\d{1,3})")
,这个正则是匹配1位到3位的数字,这不刚好,只能识别日志文件后缀1-999,999以上就无法匹配了。
同样我们也找到了相关的issue
I found cause.
Check the toRegexForFixedDate() method in ch.qos.logback.core.rolling.helper.FileNamePattern.java
Regular expression hardcoded like this:
buf.append(“(\ \d{1,3})”);
So, files indexed more than 3-digit number are not visible to delete…
I don’t know why the expression hardcoded.
Anyway, you’d better modify the source.
有人提到了现在已经修复
this issue fixed in 1.3.0-alpha1
果然这里存在问题,1.3.0-alpha1版本修复这个
但是为什么出现了前几天日志文件保留了后缀999以上,当天存在部分低于999的呢?其实这个也很好理解,还是上面源码,先正则匹配到文件之后循环累加大小,maxFileSize =20MB,totalSizeCap =5GB= 5120MB,等于256个文件,256不能被999整除,还会剩余231,也就是说第一天清理了768个文件后,剩余 231 * 20MB<5120MB ,所以不会清理掉,第二天才会继续累加这部分,然后删除上一天剩余的后缀小于1000的日志文件,这样就导致每天可能剩余一定量的后缀小于1000的日志文件,直到第二天被清理,这和我们的实际情况是相符的。
顺便吐槽一下,从GitHub的文件history来看,2012年作者已经改过一次这个正则了,把d{1,2}改成 d{1,3} ,真是醉了┑( ̄Д  ̄)┍
问题已经找出,那么解决就很简单,升级logback版本就行,当然我觉得首先这个日志打印问题就很大,一天打印上千个日志文件,大小几十G,这根本没有日志的意义了,纯属浪费资源。
]]>
参考📚:
https://tidyko.com/posts/589711b0.html
https://jira.qos.ch/browse/LOGBACK-1500
https://jira.qos.ch/browse/LOGBACK-1297
如何在Linux系统中更改一个正在执行的程序的标准输出重定向到其他文件?事情的场景是这样的,由于同事的疏忽,忘了关闭一个springboot微服务的控制台日志输出,而这个进程启动后又会把标准输出和标准错误输出写到一个process.log的日志文件中,由于控制台信息输出太多,导致长时间日志磁盘占用过大,这时候又来了一个骚操作,直接把这个日志文件删除掉了😓。
直接删除文件这个肯定是没用的,通过rm删除文件将会从文件系统的目录结构上解除链接(unlink),然而如果文件是被打开的(有进程正在使用),那么进程将仍然可以读取该文件,磁盘空间也一直被占用,这样就会导致删除了文件,但是磁盘空间却未被释放,大量的文件句柄无法释放。
执行 lsof|grep [pid]|grep deleted
查看,确实出现了文件句柄泄露😲
然后查看对应进程的文件文件描述符(fd:file descriptor)ls -l /proc/[pid]/fd
可以看到文件描述符1和2都指向了被删除的日志文件
那么如何解决这个问题呢?杀掉进程是最简单的方法,但如果不重启呢?从上面fd表上我们看到1和2指向删除文件,那么我们能不能更改这个指向,重定向到/dev/null丢掉输出呢❔
答案是有的,主要依赖于Linux 的 close()、open()、dup2()函数。(open函数也可用creat函数替换)
close函数用于关闭一个已打开的文件,函数原型如下:1
2
3int close(int filedes);
返回值:若成功则返回0,出错则返回-1
参数:filedes是文件描述符。
open函数可以打开或创建一个文件,函数原型如下:1
2
3
4int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
参数:pathname参数是要打开或创建的文件名,flags参数有一系列常数值,具体不在此处介绍
dup2函数可以复制一个文件的描述符,函数原型如下:1
2
3int dup2( int oldfd, int targetfd )
返回值:目标的文件描述符
参数:oldfd源描述符,targetfd目标描述符
简单来说,close关闭一个文件描述符,open打开一个文件并返回文件描述符,dup2将目标文件描述符变成源文件描述符的一个复制,即两个文件描述符都指向了源文件描述符指向的文件上去。所以我们可以关闭掉标准输出fd1,然后open /dev/null获得一个新的文件描述符,再将fd1指向/dev/null,这样就完成了重定向一个标准输出。
OK,我们用GDB尝试一下,首先gdb -p [pid]
进入gdb调试💻1
2
3
4
5(gdb) p close(1)
$1 = 0
(gdb) p dup2(open("/dev/null", 2), 1) //2表示O_RDWR 0x0002
$2 = 1
(gdb) quit
这样就完成标准输出重定向到/dev/null,标准错误输出依此类推
再次查看对应进程的文件描述符列表,1和2都成功指向了/dev/null,并且文件句柄也被释放掉✌
不仅仅适用于这个场景,这个方法也可以把一个忘记了nohup启动的进程放置到后台运行等等其他方面。
服务新增一个加解密模块后,本地调试OK部署到测试环境启动开始功能测试,却发现相关功能异常,查看服务日志发现以下异常
1 | java.security.NoSuchAlgorithmException: Algorithm HmacSHA256 not available |
服务是springboot的项目,直接打包成jar包,启动脚本中使用java -jar
的形式启动。由于用到了一些公司内部封装的依赖jar包,而这些jar必须外部加载不能打进jar里面启动,所以使用了-Djava.ext.dirs
去加载外部依赖jar包,这时一个陷阱就出现了,-Djava.ext.dirs
会覆盖掉java本身的ext设置,java.ext.dirs
指定的目录由ExtClassLoader加载器加载,如果没有指定该系统属性,那么该加载器默认加载$JAVA_HOME/jre/lib/ext
目录下的所有jar文件
1 | -rwxr-xr-x 1 3860502 Mar 15 2017 cldrdata.jar |
所以,只单单指定了额外依赖的jar包后,就会导致ext目录下的jar包无法加载,而这次我们新增的加解密模块使用了HmacSHA256
算法,依赖于sunjce_provider.jar
包的内容,当我们在本地环境调试时,直接IDEA启动,没有出现依赖加载错误的问题,而当在测试环境使用启动脚本启动并且指定了-Djava.ext.dirs
就导致了依赖出错。
问题已经找出,那么解决就很简单,-Djava.ext.dirs
引入多个路径加入Java自带ext路径即可
1 | java -Djava.ext.dirs=../lib:$JAVA_HOME/jre/lib/ext -jar |
在使用mybatis查询一对多结果返回对象的场景中,当主表关联的多条数据完全一致时,返回的对象只有第一条数据
1 | select |
ID | USER_ID | RESULT_ID | RATE |
---|---|---|---|
100000000000001892 | abc | 10000001 | 111 |
100000000000001892 | abc | 10000001 | 111 |
100000000000001892 | abc | 10000001 | 111 |
映射的xml如下
1 | <resultMap id="xxx" type="xxx"> |
按照设想,返回的 ID=100000000000001892的对象里面的resultList应该包含三条完全一样的数据,但是实际的结果却是只有一条
resultMap中如果不定义类似主键之类的能够区分每一条结果集的字段的话,当数据完全一致的时候会引起后面一条数据覆盖前面一条数据的现象
查询时将一对多的多表的自增主键ID也查询出来,这样resultMap映射的数据就不会完全一致,避免了这个问题,所以在使用mybatis查询的时候最好将表的主键ID查询出来,如果不需要返回ID字段,可以在代码层面实体类转换JSON返回屏蔽掉。
]]>测试环境发现了一个很奇怪的现象,一台服务器出现了请求卡顿15分钟然后才执行SQL返回结果的现象
最开始我们认为是网络问题,因为这台服务器网络环境比较特殊,正常我们环境的应用服务器和数据库服务器是同一网段的,而这台服务器和数据库服务器并不在同一网段,存在防火墙策略,但是后来发现能够稳定复现,并且每次都是发生在服务空闲一段时间后第一次请求,由于我们使用的是druid,于是查询了相关问题,果然网络上已经有了很多相关的描述
druid下莫名其妙hold15分钟+。疑是socket timeout超时15分钟后,重建了新连接导致
从连接池中获取到失效连接,在检验连接有效性时出现长时间等待,大概15分钟 #2905
大致原因是这样,当应用服务器和数据库服务器直接存在防火墙策略时,如果服务空闲时间过长,会被防火墙主动断开数据库连接,但是此时druid并未感知,此时当有请求过来时,如果druid配置了testWhileIdle
(申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis
,执行validationQuery
检测连接是否有效。),那么将进行一次检测,检测的方式也是根据配置的策略,一般是select 1 from dual
,由于这个连接已经被防火墙断掉,根本到达不了数据库,而druid这边则在一直等待,而这个等待的超时受到socket timeout
限制,而我们服务器本身的系统socket timeout
设置就是15分钟,所以druid会一直hold15分钟,直到触发超时重新建立连接。所以问题就出现了druid无法主动恢复防火墙主动断开的连接,只有当触发超时才能进行重建连接。
问题解决的思路有两种,第一钟思路,更改jdbc的socke timeout
,但是不建议,因为如果要能够快速重连,那么这个socket timeout
就需要配置很短,但是这个时间很短会导致执行过长的SQL无法返回结果,socket timeout
必须大于statement timeout
,否则socket timeout
先生效则statement timeout
毫无意义,所以即使配置几分钟还是会出现请求hold的现象,因此不合适。
第二种思路,既然长时间空闲后连接会被防火墙断开,那么维持一个心跳,不让连接被防火墙断开即可,因此,需要引入druid的keepAlive
引入druid GitHub上对此配置的解释
在Druid-1.0.27之前的版本,DruidDataSource建议使用TestWhileIdle来保证连接的有效性,但仍有很多场景需要对连接进行保活处理。在1.0.28版本之后,新加入keepAlive配置,缺省关闭。使用keepAlive功能,建议使用1.1.16或者更高版本
打开KeepAlive之后的效果
- 初始化连接池时会填充到minIdle数量。
- 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
- 当网络断开等原因产生的由ExceptionSorter检测出来的死连接被清除后,自动补充连接到minIdle数量。
开启连接保活配置keepAlive
,对于minIdle
以内的连接,按照timeBetweenEvictionRunsMillis
间隔进行保活检测,当空闲时间大于minEvictableIdleTimeMillis
,发送心跳保持连接活跃,只要发送保活心跳的时间间隔小于防火墙断开空闲连接的时间即可。对于下面配置,只有1个空闲连接在空闲状态下会在5分钟左右进行心跳,保持长时间存活。
1 | minIdle: 1 |
A/B 系统分区是 Google 在 Android 7.0 时代引入的新机制,采用这个机制的设备拥有 A、B 两套系统分区,用户数据则能够在这两套系统分区之间共用。
这种分区机制带来的最大好处是无缝系统更新(seemless updates),当我们在 A 系统中进行 OTA 更新时,而实际更新的是另个一并未启用的 B 系统。手机重启后,系统分区从 A 切换到 B新系统,介于此机制,我们可以实现OTA升级后仍然保留Magisk的Root权限。由于使用了 A/B 分区,因此没有独立的 Recovery 分区;Recovery 现在是 Boot 的一部分。所以要通过 fastboot boot 实现临时从指定镜像启动,从而进入 TWRP,并通过刷入 twrp-installer 实现对 TWRP 的持久化。
连接电脑,在cmd窗口内输入adb命令:
1 | fastboot devices # 检查设备是否连接 |
回车,手机即出现解锁确认界面,音量键进行选择-「UNLOCK THE BOOTLOADER」,按电源键确认,手机重启开始解锁
需要如下:
执行以下操作:
adb reboot bootloader
进入 Bootloader 界面fastboot boot twrp.img
进入临时 TWRP需要如下:
执行以下操作:
需要如下:
执行以下操作:
adb reboot bootloader
进入 Bootloader 界面fastboot boot TWRP.img
进入临时 TWRPadb sideload Magisk-vX.X.zip
fastboot reboot
重新启动手机需要如下:
执行以下操作:
install —— 修补 boot 镜像文件
),将获得的 magisk_patched.img
传回电脑。再次进入 Bootloader,输入
1 | fastboot boot magisk_patched.img |
来加载生成后的 boot 分区文件获取临时 root
fastboot flash recovery magisk_patched.img
修复recovery image安装(install)——install——Direct Install(直接安装)
)。在 Magisk Manager 中下载并安装插件 TWRP A/B Retenion Script
Install to Inactive Slot (After OTA)
)模拟环境中我们发现一台主备的Consumer进程无法处理消息,首先我们查看服务订阅情况,发现服务订阅正常,日志显示正常收到消息,但是Listener收到消息后业务代码未执行,导致触发消息超时反馈。
为什么业务代码不执行呢?查看业务代码日志,发现在一次处理中,执行了一次Rest请求后,剩余代码未执行,整个线程hold住了,是否由于该线程阻塞导致Listener收到消息后无法处理呢?梳理了一下代码逻辑,我们采用的是Guava EventBus的事件监听和发布订阅模式,业务方法使用@Subscribe订阅到消息后进行处理,而订阅者对象在处理事件时是使用了synchronized同步锁,所以是因为锁一直未释放导致其他消息无法订阅处理吗?为什么线程会卡住呢?
接下来我们使用JDK的jstack工具打印出服务的线程堆栈信息,确实发现有线程Blocked并且等待锁的情况。
1 | "IMT_328b84d5-3364-46cf-acfe-5e78de9f9cce_BL" prio=10 tid=0xada04400 nid=0x7816 waiting for monitor entry [0x9d1fe000] |
waiting to lock <0xb73e5c58> (a com.google.common.eventbus.Subscriber$SynchronizedSubscriber)
从这个信息看确实是Subscribe的同步锁,那么继续寻找当前占有锁的线程,发现如下
1 | "IMT_0945732d-7dc7-4d12-8e5f-c4f7e9f9f295_BL" prio=10 tid=0xa7d02c00 nid=0x68ce runnable [0x9e9fc000] |
从以上信息可以看到,线程一直runnable状态,锁释放不了,代码最后执行到java.net.SocketInputStream.socketRead0
发生阻塞,继续向下看,由Apache Httpclient调用,再往后是Ribbon。而我们的服务确实是通过Ribbon管理目标服务IP发送Rest请求,由于本服务器网络特殊,并未注册到Eureka管理,而是直接把多活的目标服务列表写在配置中维护,加上之前日志的分析,线程确实是卡在Rest请求上,这就很奇怪了,Connection Timeout和Read Timeout我们都有设置,为何未生效?而且这个问题只在本环境发生,并且重启后依然稳定复现。首先对访问的目标服务器端口telnet,一切正常,而执行netstat查看服务器的网络连接,发现本机与一个目标服务端口一直存在一个ESTABLISHED的tcp连接,是否这个一直阻塞着?
查阅了一下socketRead0阻塞的问题,从网上资料看,这个问题确实有存在,并且从一篇博客从锁死的RUNNABLE线程谈UNIX的I/O模型中发现这样写道,JAVA IO库(java version “1.8.0_131”)的一个坑。
由于某些请求的TCP包传输过程中出现异常导致
poll
在没有真实可读数据情况下返回可读标识,使得阻塞的recv
方法永远阻塞下去,从而使得当前线程一直处于RUNNABLE
,当线程池的核心线程都被这种线程占据之后,就再也无法处理新提交的任务了。
现在open jdk已经修复了这一bug SocketInputStream.socketRead0 can hang even with soTimeout set
但是从这个来看,发生的概率应该很低的,而我们服务却是百分百发生,重启后依旧,所以继续从堆栈信息上分析。
从堆栈信息上分析可知Ribbon在更新服务列表updateAllServerList,然后执行了PingUrl的isAlive,调用了HttpClient发生了阻塞。默认情况下服务启动后Ribbon并不会直接加载服务列表,而是当第一次Rest请求调用时,Ribbon会去加载服务列表,并且执行设置的PingUrl方法判断服务节点是否存在,加载好服务列表之后根据设置的Loadbalance策略调用服务节点,该线程应该是hold在PingUrl这一步上。
PingUrl是服务中Ribbon默认设置的ribbonPing实现,用于检测服务IP列表。翻看PingUrl中isAlive方法的源码,大致如下:
1 | String urlStr = ""; |
可以看出实际上就是调用httpClinet发送了一次GET请求,请求的URL就是服务列表的每一个服务端口+指定的URL。既然如此,我们可以直接模拟同样的GET请求,执行curl命令
1 | curl -X GET 'http://server:port' |
果然,有一台目标服务器没有响应,一直卡住,这台服务器正是上面tcp一直处于ESTABLISHED状态的服务器。看来问题应该是由于这台服务器导致的,登陆这台服务器之后,df -h直接卡住不显示结果,怀疑用户目录下共享存储出问题了,cd到用户目录下,执行ls依然卡住未返回,找运维同学处理后,再次执行curl后正常,而服务还是阻塞状态,重启后ESTABLISHED状态tcp连接释放,服务恢复,正常处理消息。
由于一台服务器共享存储挂了,导致服务器上tomcat服务无法处理远程的请求,没有任何返回,连接一直处于ESTABLISHED状态未释放,虽然telnet服务端口是正常的,但是http请求会hold住无响应。而consumer服务使用Ribbon管理目标服务节点,当服务重启后,第一次执行Rest请求,Ribbon会去加载服务列表并进行ribbonPing检测所有的目标节点是否异常,默认是通过一个GET请求,因此当检测到故障的目标节点后,连接释放不了,线程一直hold,而Subscribe的同步锁也一直被线程占用无法释放,导致其他消息过来时无法处理从而超时,而重启后依然重现这一过程导致问题依旧。
]]>今天测试过程中一个Oracle mybatis批量插入数据的代码报出了一个异常
Caused by: java.lang.ArrayIndexOutOfBoundsException: -32768
具体异常堆栈信息如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14Caused by: java.lang.ArrayIndexOutOfBoundsException: -32768
at oracle.jdbc.driver.OraclePreparedStatement.setupBindBuffers(OraclePreparedStatement.java:2673)
at oracle.jdbc.driver.OraclePreparedStatement.processCompletedBindRow(OraclePreparedStatement.java:2206)
at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3365)
at oracle.jdbc.driver.OraclePreparedStatement.execute(OraclePreparedStatement.java:3476)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3409)
at com.alibaba.druid.wall.WallFilter.preparedStatement_execute(WallFilter.java:619)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3407)
at com.alibaba.druid.filter.FilterAdapter.preparedStatement_execute(FilterAdapter.java:1080)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3407)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:440)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3407)
at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:167)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:498)
感觉很奇怪,查看日志SQL打印,这个方也就拼接了400条SQL,参数也不是很多,于是从日志里面copy了一下接口入参在本地用Postman debug了一下,结果第一次居然入库成功,再执行一次,出现了同样的错误,再执行,又成功……反复如此,顿时懵逼。看异常信息,执行ojdbc包内的oracle.jdbc.driver.OraclePreparedStatement.setupBindBuffers
方法数组下标越界,好吧,放Google搜一下,发现一段这样描述
The 10g driver apparently keeps a global serialnumber for all parameters in the entire batch, with a “short”variable. So you can have at most 32768 parameters in the batch. I was havingthe same exception because I have a INSERT statement with 42 parameters and mybatches can be as big as 1000 records, so 42000 > 32768 and this overflowsto a negative index. I reduced the batch factor to 100 to be safe, and all iswell. I guess your update DML should have a larger number of parameters perrecord, right? (My diagnostic of the bug is just deduction from the symptoms)
https://community.oracle.com/thread/599441?start=15&tstart=0>
说是10g driver statement最大允许参数个数为32768,超过会报错。似乎有点类似,但是我只插入了400条啊,而且每个SQL参数只有9个,也就是3600个参数,远小于32768。
还有另外一个说法
In Oracle Metalink (Oracle’s support site - Note ID 736273.1) I found that this is a bug in JDBC adapter (version 10.2.0.0.0 to 11.1.0.7.0) that when you call preparedStatement with more than 7 positional parameters then JDBC will throw this error.
https://stackoverflow.com/questions/277744/jdbc-oracle-arrayindexoutofboundsexception
感觉也不符合,但是从搜索的结果看,10g 的 ojdbc似乎确实有些问题,于是看了下pom,乖乖1
2
3
4
5<dependency>
<groupId>ojdbc14</groupId>
<artifactId>ojdbc14</artifactId>
<version>10.2.0.4.0</version>
</dependency>
那么换个版本吧,我们数据库是11.2g的,于是换了个ojdbc61
2
3
4
5<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.4.0</version>
</dependency>
嗯,这次很顺利,没有再出现异常,看来ojdbc14确实有些问题,但是还是比较疑惑,单单400条数据,每条9个参数就已经超过限制了吗?1
2
3
4
5
6
7
8
9
10<insert id="insertBatch" parameterType="java.util.List">
INSERT INTO table (a, b, c, d ......
)
SELECT SEQ.nextval,A.* FROM (
<foreach collection="list" item="item" index="index" separator="union all">
SELECT
#{item.a},#{item.b},#{item.c},#{item.d}......
FROM dual
</foreach>) A
</insert>
拼接下来实际SQL如下,类似于insert into tableA select * from tableB1
2
3
4
5
6
7
8
9
10
11
12
13INSERT INTO table (a, b, c, d ......)
SELECT
SEQ.nextval,
A.*
FROM
( SELECT ?, ?, ?, ?, ?, ?, ?, ? FROM dual
union all
SELECT ?, ?, ?, ?, ?, ?, ?, ? FROM dual
union all
SELECT ?, ?, ?, ?, ?, ?, ?, ? FROM dual
union all
......
) A
实际debug了一下,确实出异常的时候OraclePreparedStatement.setupBindBuffers方法short数组 bindIndicators大小超过了32768,换成ojdbc6的时候该方法未调用。
]]>感觉可能昨天都跪了,一直没注意,今天晚上才发现只剩一个固态C盘了,设备管理器也找不到机械硬盘,拔了重插也不转,心塞,明明这块1T日立也就2年多啊,通电也就6000h,我对日立还特有好感的说,买了好几块了,
想到好多东西也没备份就蛋疼
从各个论坛收集的歌曲、图包、漫画,还有各种软件、文档、配置等等,虽然一部分网盘有备份,但是全部拖下来也得费不少功夫,而且有些东西完全想不起了。。。唯一庆幸的是博客的之前备份了一份到Git上,hexo和主题多设备同步,o(︶︿︶)o 唉,以后还是多做备份吧。
新买的2T西数蓝盘,空空如也。
]]>这周末版本升级遇到一个Jedis异常,其中一步是从Mysql中的临时表查找数据然后拼装key从Redis中查找对应的缓存数据并修改。然而升级过程中修数程序却抛出一个异常Unexpected end of stream
意外停止。
1 | redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream. |
Redis采用的是codis搭建的集群,我们立即Telnet访问的端口,并在服务器使用Redis-cli直接连接,结果都未发现异常。于是立即去网上查找相关资料,网上都这样描述此异常。
客户端缓冲区异常
这个异常是客户端缓冲区异常,产生这个问题可能有三个原因:
多个线程使用一个Jedis连接。
客户端缓冲区满了,Redis有三种客户端缓冲区:
普通客户端缓冲区(normal):用于接受普通的命令,例如get、set、mset、hgetall、zrange等。
slave客户端缓冲区(slave):用于同步master节点的写命令,完成复制。
发布订阅缓冲区(pubsub):pubsub不是普通的命令,因此有单独的缓冲区。
Redis客户端缓冲区配置的格式是:
1 | client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds> |
class: 客户端类型:可选值为normal、slave和pubsub。
hard limit: 如果客户端使用的输出缓冲区大于hard limit,客户端会被立即关闭,单位为秒。
soft limit和soft seconds: 如果客户端使用的输出缓冲区超过了soft limit并且持续了soft limit秒,客户端会被立即关闭,单位为秒。
长时间闲置连接会被服务端主动断开,可以查询timeout配置的设置以及自身连接池配置确定是否需要做空闲检测。
于是我们立即对配置进行了检查,并未发现有相关问题,timeout默认设置为0,客户端缓冲区临时修改为不限制也未见生效。
这下犯愁了,由于这个程序几乎没有日志,代码也不是我们编写,而是一位外地的同事提供的。只能一波人紧急分析代码,一波人继续查询错误日志,从异常的堆栈中我们发现异常是从执行redis的一个get方法抛出的,难道是get某个key的时候出现了异常?由于日志中没有打印具体的key信息,所以也不清楚具体情况,难道是某个key的体积过大,导致查询的时候超过了限制?于是立即使用bigkeys查询了一下Redis的大体积key,结果最大的也只有十几kb,很显然这也不是原因。大家正在一筹莫展,准备把程序加上详细的日志再具体分析,但是生产环境做紧急变更交付物是很困难的,之前我们已经在模拟环境同步生产数据测了好几轮,都未发现问题,为何生产就出现问题了呢?
继续看codis porxy的日志,发现了一个特殊的地方,客户端建立的连接每次都是经过60s后被断开,显示EOF错误,代表客户端客户端主动断开,于是我们立即查找相关配置,是否存在60s的配置,这时候运维同学提到了一件事,codis-porxy使用了nginx做负载均衡代理,nginx应该做了超时配置,于是我们翻看了nginx配置,果然存在一个60s的超时配置,而这时候我们再去翻看代码逻辑,发现了一个问题,首先程序使用jedis建立一个redis连接,然后从MySQL从查找临时表所有数据,然后修改redis缓存。而我们的临时表数据过于庞大,而且缺乏索引,所以查询这一步花费了很长时间,已经超过60s,等数据查询完毕,再执行redis操作,此时一直空闲的连接已经被nginx当作超时给断开了。为何之前模拟环境并未出现这个问题?应该是最近待修的数据又增加了很多,刚好超过60s,导致这个问题并未在模拟阶段发现。
于是我们在模拟环境复现该问题,并临时把nginx配置修改为600s,于是修数程序正常执行,没有异常,问题到此解决,于是生产同步操作,最终完成了升级。
]]>NS入手一年多了,一直拿张32G的microSD卡凑合着,因为入手游戏不多,又都是实体版,所以下载几个DLC也完全够了,不过考虑到实体版换卡还是麻烦,而且今年感兴趣的游戏也不少,有些不打算买实体了,所以一直想找个机会换张大一点的microSD卡,正好狗东200G闪迪卡活动195,价格还算OK,于是入手换之。
1. 取卡:NS 关机(Power Off),取出原来的 MicroSD 卡,并把MicroSD卡内容备份到PC
2. NS插入新卡(可能需要更新系统)
3. 格式化新卡(建议):设置 - 系统设置 - 格式化选项 - 格式化 microSD 卡,格式化新卡。
4. 拷贝游戏文件:将新卡里的 Nintendo 文件夹删除,同时将旧卡备份到PC里的 Nintendo 文件夹复制到新卡里
5. 插入新卡:在关机的状态下插入新卡,然后开机,检查下载内容是否存在。
TCP(Transmission Control Protoco)
,是一种基于字节流面向连接的传输层协议。数据的传输需要通信双方建立一个连接,TCP协议采用三次握手建立一个连接,采用 4 次挥手来关闭一个连接。每一个TCP连接都有两个端点,叫作套接字(socket),它的定义为IP地址+端口号拼接。
一个 TCP 连接由一个 4 元组构成,分别是两个 IP 地址和两个端口号。一个 TCP 连接通常分为三个阶段:启动、数据传输、关闭。
当 TCP 接收到另一端的数据时,它会发送一个确认,但这个确认不会立即发送,一般会延迟一会儿。ACK 是累积的,一个确认字节号 N 的 ACK 表示所有直到 N 的字节(不包括 N)已经成功被接收了。这样的好处是如果一个 ACK 丢失,很可能后续的 ACK 就足以确认前面的报文段了。
一个完整的 TCP 连接是双向和对称的,数据可以在两个方向上平等地流动。给上层应用程序提供一种双工服务。一旦建立了一个连接,这个连接的一个方向上的每个 TCP 报文段都包含了相反方向上的报文段的一个 ACK。
序列号的作用是使得一个 TCP 接收端可丢弃重复的报文段,记录以杂乱次序到达的报文段。因为 TCP 使用 IP 来传输报文段,而 IP 不提供重复消除或者保证次序正确的功能。另一方面,TCP 是一个字节流协议,绝不会以杂乱的次序给上层程序发送数据。因此 TCP 接收端会被迫先保持大序列号的数据不交给应用程序,直到缺失的小序列号的报文段被填满。
状 态 | 描 述 |
---|---|
CLOSED | 关闭状态,没有连接活动或正在进行 |
LISTEN | 监听状态,服务器正在等待连接进入 |
SYN_RCVD | 收到一个连接请求,尚未确认 |
SYN_SENT | 已经发出连接请求,等待确认 |
ESTABLISHED | 连接建立,正常数据传输状态 |
FIN_WAIT_1 | (主动关闭)已经发送关闭请求,等待确认 |
FIN_WAIT_2 | (主动关闭)收到对方关闭确认,等待对方关闭请求 |
TIMED_WAIT | 完成双向关闭,等待所有分组死掉 |
CLOSING | 双方同时尝试关闭,等待对方确认 |
CLOSE_WAIT | (被动关闭)收到对方关闭请求,已经确认 |
LAST_ACK | (被动关闭)等待最后一个关闭确认,并等待所有分组死掉 |
那么状态转换为什么要经历三次握手和四次挥手呢?
为什么需要第三次客户端ACK?
以客户端主动关闭为例,服务器端也可以主动关闭,方向与下面相反。
为什么主动关闭连接方最后需要等待2MSL?
为什么关闭连接需要四次挥手,比建立连接多一次呢?
如果通信双方同时请求连接或同时请求释放连接?
TIME_WAIT状态
]]>
参考📚:
TCP的三次握手与四次挥手
使用hexo可以生成静态网页部署到GitHub和VPS上搭建个人博客,但是hexo的部署都是在本地,如果换了一套环境如何也能够编辑发布自己的博客网站呢?
由于部署博客已经使用了github仓库托管网页代码,我们可以考虑使用这个来做hexo部署发布管理的版本控制,由于部署的网站默认使用了master分支,因此我们可以使用一个新的分支hexo或者新建一个仓库来管理。
下面步骤默认已经安装好了hexo并且已经成功部署网站,首先切换到hexo主目录,git init进行初始化,如果已经纳入git管理并且关联了远程仓库,可能需要删除重新关联。
1 | git 初始化 |
到这一步似乎已经大功告成了,然而,如果你使用了第三方主题,并且是直接git clone,你会发现一个问题,上传的themes/下面主题是空目录,因为git无法直接管理这样的嵌套模块,那么该怎么做呢?最暴力,直接取消主题模块的git管理,但是这样后续主题模块的更新就是一个问题了,所以不推荐,好在我们可以通过git的submodule或subtree来实现,对于git clone安装的其它主题和插件都可以按照这个思路。
简单来说,submodule 和 subtree 最大的区别是,submodule 保存的是子仓库的 link,而 subtree 保存的是子仓库的 copy。
child 目录被当做一个独立的 Git 仓库,所有的 Git 命令都可以在 child 目录以及上层项目下独立工作。尽管 child 是子目录,当你不在 child 目录时并不记录它的内容。而当你在那个子目录里修改并提交时,子项目会通知那里的 HEAD 已经发生变更并记录你当前正在工作的那个提交。而此时上层项目会显示 child 目录下的改动,将它记录成来自那个仓库的一个特殊的提交。
若他人要克隆该项目,会发现 child 目录为空。这时需要执行 git submodule init 来初始化你的本地配置文件,以及 git submodule update 拉取数据并切换到合适的提交。而后每次从主项目拉取子模块的变更时,由于主项目只更新了子模块提交的引用而没有更新子模块目录下的代码,必须执行 git submodule update 来更新子模块代码。
不同于 git submodule,此时的 child 仅仅是含有相关代码的普通目录,而不是一个独立的 Git 仓库。因此当在 child 进行修改时,上层项目会立刻记录其改动,而不是像之前那样先在子项目中提交才能进行记录。克隆上层仓库时 child 目录也不再为空。但同时,child 也不能再执行独立的 Git 命令,只有 git subtree 相关的操作。
进行操作前请先备份已有的next主题目录,根据不同情况操作中可能需要删除并且重新clone下来
1 | 首先在自己的github上fork一份next源码 |
上面操作已经把hexo的源目录同步到Git,因此我们只需要clone,并且安装node.js和hexo环境
1 | git clone -b hexo git@github.com:Elietio/Elietio.github.io.git |
awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各种分析处理。
awk 命令和 sed 命令结构相同,通常情况下,awk 将每个输入行解释为一条记录而每一行中的内容(由空格或者制表符分隔)解释为每一个字段,一个或者多个连续空格或者制表符看做定界符。awk 中 $0
代表整个记录。
1 | awk ' /MA/ { print $1 }' list |
解释:打印包含 MA 的行中的第一个单词。再举一个具体的例子,比如
1 | echo 'this is one world\nthat is another world' | awk '{print $1}' |
那么输出就是 awk 处理之后的每一行第一个字符也就是
1 | this |
awk 命令的基本格式
1 | awk [options] 'script' file |
options
这个表示一些可选的参数选项,script
表示 awk 的可执行脚本代码(一般被{}
花括号包围),这个是必须的。file
这个表示 awk 需要处理的文件,注意需要是纯文本文件(意味着 awk 能够处理)。
之前提到的awk 默认的分割符为空格和制表符,awk 会根据这个默认的分隔符将每一行分为若干字段,依次用 $1
, $2
,$3
来表示,可以使用 -F
参数来指定分隔符
1 | awk -F ':' '{print $1}' /etc/passwd |
解释:使用 -F
来改变分隔符为 :
,比如上面的命令将 /etc/passwd 文件中的每一行用冒号 :
分割成多个字段,然后用 print 将第 1 列字段的内容打印输出
在 awk 中同时指定多个分隔符,比如现在有这样一个文件 some.log 文件内容如下
1 | Grape(100g)1980 |
现在我们想将上面的 some.log 文件中按照 “水果名称(重量)年份” 来进行分割
1 | $ awk -F '[()]' '{print $1, $2, $3}' some.log |
在 -F
参数中使用一对方括号来指定多个分隔符,awk 处理 some.log 文件时就会使用 “(“ 或者 “)” 来对文件的每一行进行分割。
awk 除了 $
和数字表示字段还有一些其他的内置变量:
比如我们有这么一个文本文件 fruit.txt 内容如下,用它来演示如何使用 awk 命令工具
1 | peach 100 Mar 1997 China |
文件的每一行的每一列的内容除了可以用 print 命令打印输出以外,还可以对其进行赋值
1 | awk '{$2 = "***"; print $0}' fruit.txt |
上面的例子就是表示通过对 $2
变量进行重新赋值,来隐藏每一行的第 2 列内容,并且用星号 *
来代替其输出
在参数列表中加入一些字符串或者转义字符之类的东东
1 | awk '{print $1 "\t" $2 "\t" $3}' fruit.txt |
像上面这样,你可以在 print 的参数列表中加入一些字符串或者转义字符之类的东东,让输出的内容格式更漂亮,但一定要记住要使用双引号。
awk 内置 NR 变量表示每一行的行号
1 | awk '{print NR "\t" $0}' fruit.txt |
awk 内置 NF
变量表示每一行的列数
1 | awk '{print NF "\t" $0}' fruit.txt |
awk 中 $NF
变量的使用
awk '{print $NF}' fruit.txt
上面这个 $NF
就表示每一行的最后一列,因为 NF
表示一行的总列数,在这个文件里表示有 5 列,然后在其前面加上 $
符号,就变成了 $5
,表示第 5 列
1 | awk '{print $(NF - 1)}' fruit.txt |
上面 $(NF-1)
表示倒数第 2 列, $(NF-2)
表示倒数第 3 列,依次类推。
1 | awk 'NR % 6' # 打印出了 6 倍数行之外的其他行 |
awk 还提供了一些内置函数,比如
toupper()
用于将字符转为大写tolower()
将字符转为小写length()
长度substr()
子字符串sin()
正弦cos()
余弦sqrt()
平方根rand()
随机数更多的方法可以参考 man awk
1 | awk '{print FILENAME "\t" $0}' demo1.txt demo2.txt |
当你使用 awk 同时处理多个文件的时候,它会将多个文件合并处理,变量FILENAME
就表示当前文本行所在的文件名称。
在脚本代码段前面使用 BEGIN 关键字时,它会在开始读取一个文件之前,运行一次 BEGIN
关键字后面的脚本代码段, BEGIN 后面的脚本代码段只会执行一次,执行完之后 awk 程序就会退出
1 | awk 'BEGIN {print "Start read file"}' /etc/passwd |
awk 脚本中可以用多个花括号来执行多个脚本代码,就像下面这样
1 | awk 'BEGIN {print "Start read file"} {print $0}' /etc/passwd |
awk 的 END 指令和 BEGIN 恰好相反,在 awk 读取并且处理完文件的所有内容行之后,才会执行END
后面的脚本代码段
awk 'END {print "End file"}' /etc/passwdawk 'BEGIN {print "Start read file"} {print $0} END {print "End file"}' /etc/passwd
可以在 awk 脚本中声明和使用变量
1 | awk '{msg="hello world"; print msg}' /etc/passwd |
awk 声明的变量可以在任何多个花括号脚本中使用
1 | awk 'BEGIN {msg="hello world"} {print msg}' /etc/passwd |
在 awk 中使用数学运算,在 awk 中,像其他编程语言一样,它也支持一些基本的数学运算操作
1 | awk '{a = 12; b = 24; print a + b}' company.txt |
上面这段脚本表示,先声明两个变量 a = 12 和 b = 24,然后用 print 打印出 a 加上 b 的结果。
请记住 awk 是针对文件的每一行来执行一次单引号 里面的脚本代码,每读取到一行就会执行一次,文件里面有多少行就会执行多少次,但 BEGIN 和 END 关键字后脚本代码除外,如果被处理的文件中什么都没有,那 awk 就一次都不会执行。
awk 还支持其他的数学运算符
1 | + 加法运算符 |
在 awk 中使用条件判断
比如有一个文件 company.txt 内容如下
yahoo 100 4500google 150 7500apple 180 8000twitter 120 5000
如果要判断文件的第 3 列数据,也就是平均工资小于 5500 的公司,然后将其打印输出
1 | awk '$3 < 5500 {print $0}' company.txt |
上面的命令结果就是平均工资小于 5500 的公司名单,$3 < 5500
表示当第 3 列字段的内容小于 5500 的时候才会执行后面的 {print $0} 代码块
1 | awk '$1 == "yahoo" {print $0}' company.txt |
awk 还有一些其他的条件操作符如下
运算符 | 描述 |
---|---|
< | 小于 |
<= | 小于或等于 |
== | 等于 |
!= | 不等于 |
> | 大于 |
>= | 大于或等于 |
~ | 匹配正则表达式 |
!~ | 不匹配正则表达式 |
使用 if 指令判断来实现上面同样的效果
1 | awk '{if ($3 < 5500) print $0}' company.txt |
上面表示如果第 3 列字段小于 5500 的时候就会执行后面的 print $0
比如现在我们有这么一个文件 poetry.txt 内容如下:
1 | This above all: to thine self be true |
使用正则表达式匹配字符串 “There” ,将包含这个字符串的行打印并输出
1 | awk '/There/{print $0}' poetry.txt |
使用正则表达式配一个包含字母 t 和字母 e ,并且 t 和 e 中间只能有任意单个字符的行
1 | awk '/t.e/{print $0}' poetry.txt |
如果只想匹配单纯的字符串 “t.e”, 那正则表达式就是这样的 /t.e/ ,用反斜杠来转义 . 符号 因为 . 在正则表达式里面表示任意单个字符。
使用正则表达式来匹配所有以 “The” 字符串开头的行
1 | awk '/^The/{print $0}' poetry.txt |
在正则表达式中 ^
表示以某某字符或者字符串开头。
使用正则表达式来匹配所有以 “true” 字符串结尾的行
1 | awk '/true$/{print $0}' poetry.txt |
在正则表达式中 $ 表示以某某字符或者字符串结尾。
1 | awk '/m[a]t/{print $0}' poetry.txt |
上面这个正则表达式 /m[a]t/
表示匹配包含字符 m ,然后接着后面包含中间方括号中表示的单个字符 a ,最后包含字符 t 的行,输出结果中只有单词 “matter” 符合这个正则表达式的匹配。因为正则表达式 [a] 方括号中表示匹配里面的任意单个字符。
继续上面的一个新例子如下
1 | awk '/^Th[ie]/{print $0}' poetry.txt |
这个例子中的正则表达式 /^Th[ie]/ 表示匹配以字符串 “Thi” 或者 “The” 开头的行,正则表达式方括号中表示匹配其中的任意单个字符。
再继续上面的新的用法
1 | awk '/s[a-z]/{print $0}' poetry.txt |
正则表达式 /s[a-z]/
表示匹配包含字符 s 然后后面跟着任意 a 到 z 之间的单个字符的字符串,比如 “se”, “so”, “sp” 等等。
正则表达式 [] 方括号中还有一些其他用法比如下面这些
[a-zA-Z] 表示匹配小写的 a 到 z 之间的单个字符,或者大写的 A 到 Z 之间的单个字符[^a-z] 符号 `^` 在方括号里面表示取反,也就是非的意思,表示匹配任何非 a 到 z 之间的单个字符
正则表达式中的星号 *
和加号 +
的使用方法,*
表示匹配星号前字符串 0 次或者多次,+
和星号原理差不多,只是加号表示任意 1 个或者 1 个以上,也就是必须至少要出现一次。
正则表达式问号 ? 的使用方法,正则中的问号 ?
表示它前面的字符只能出现 0 次 或者 1 次。
正则表达式中的 {} 花括号用法,花括号 {} 表示规定它前面的字符必须出现的次数,像这个 /go{2}d/ 就表示只匹配字符串 “good”,也就是中间的字母 “o” 必须要出现 2 次。
正则表达式中的花括号还有一些其他的用法如下
/go{2,10}d/ 表示字母 "o" 只能可以出现 2 次,3 次,4 次,5 次,6 次 ... 一直到 10 次/go{2,}d/ 表示字母 "o" 必须至少出现 2 次或着 2 次以上
正则表达式中的圆括号表示将多个字符当成一个完整的对象来看待。比如 /th(in){1}king/ 就表示其中字符串 “in” 必须出现 1 次。而如果不加圆括号就变成了 /thin{1}king/ 这个就表示其中字符 “n” 必须出现 1 次。
使用 awk 过滤 history 输出,找到最常用的命令1
history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head
过滤文件中重复行1
awk '!x[$0]++' <file>
将一行长度超过 72 字符的行打印1
awk 'length>72' file
查看最近哪些用户使用系统1
last | grep -v "^$" | awk '{ print $1 }' | sort -nr | uniq -c
假设有一个文本,每一行都是一个 int 数值,想要计算这个文件每一行的和,可以使用1
awk '{s+=$1} ENG {printf "%.0f", s}' /path/to/file
]]>
对于含有泛型的类,如何在类中获取泛型的class对象?
如果子类继承该类的时候传递了泛型,所以编译期该类的泛型其实已经指定了。那么在类中定义一个Class对象,然后通过构造代码块,this指向的是当前调用的子类
1 | private Class<T> clazz; |
对于没有继承该类的子类,可以采用new的时候有参构造函数传递
1 | private Class<T> clazz; |