使用httpclient的时候遇到NoHttpResponseException的处理

在使用httpclient的时候,偶尔能遇到 NoHttpResponseException 这个异常。

根据这个异常的一些信息,感觉起来很像是网络的因素导致,但是其实不是的,主要是因为以下原因导致:
1,nginx并发压力非常大的时候,排队都来不及处理了,nginx直接丢弃了,这个时候会报这个错误。
2,还有一种情况,网上没有进行任何报道,那是在 nginx reload 的时候,假如 httpclient 刚好在快速的进行请求,这个时候就会报这个异常。这是因为:nginx reload不会对现有的请求连接进行中断,但是对于keepalive的连接,假如刚好没有在进行数据传输,nginx会认为该连接已经请求完毕,所以就主动关闭了,而几乎在同时,httpclient还没有获得主动关闭的响应,于是认为该连接是可用的,用这个连接发送数据的时候,就会收到这个异常。

对于第二个情况,有一篇文章这么讲道:https://github.com/alibaba/tengine/issues/1074 ,但是对于nginx core不是随便人都能改的,同时这么改了后也不知是否有其他副作用,总归解决办法不是特别理想。

经过查找资料以及分析,大家都在说可以用重试的机制来解决,从上述2种原因来看,也确实重试是最好的解决办法,那么对于 httpclient 来讲,怎么实现重试呢?下面贴一段代码:

HttpClientBuilder.create().setRetryHandler((exception, executionCount, context) -> {
            if (executionCount > 1) {
                return false;
            }

            if(exception instanceof NoHttpResponseException) {
                log.info("NoHttpResponseException occured, will retry request");
                return true;
            }

            return false;
        }).build();

以上的代码对于重试,只是用了最简单重试,具体复杂的重试大家可以自行设计。

如何在springboot中设定keepalive timeout

查看springboot的官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tomcat.keep-alive-timeout

可以看到对server.tomcat.keep-alive-timeout这个参数的定义如下:
Time to wait for another HTTP request before the connection is closed. When not set the connectionTimeout is used. When set to -1 there will be no timeout.

可是在配置文件中按照上面所说,进行了定义:(注意,单位是毫秒,spring的文档历来是垃圾中的战斗机)

server:
  tomcat:
    keep-alive-timeout: 30000

运行后,发现不起任何作用。

于是只能按照上面所说的,对connection-timeout进行设置:

server:
  tomcat:
    connection-timeout: 30000

这样就起效了,说明keep-alive-timeout没有用,文档有误。

上述是通过配置来更改,还可以通过代码来设定keepalive_timeout,如下:

@Configuration
public class Config {

    @Bean   
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat
        .addConnectorCustomizers(new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
                ((AbstractProtocol) connector.getProtocolHandler())
                        .setKeepAliveTimeout(30000);
            }
        });
        return tomcat;
    }

}

如何在本地访问kubernetes dashboard

当在k8s集群环境中,安装好kubernetes-dashboard,我们需要通过我们自己的电脑去访问dashboard,那么需要在我们的电脑上安装 kubectl安装,别忘记配置 /etc/hosts,然后启动 kubectl proxy,最后访问:http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

我们知道,kubectl是用来输入管理k8s命令的工具(或者叫做客户端),在master节点我们会安装kubectl,在自己本机上再安装一个kubectl,总感觉没有这个必要,那能不能直接在master节点上运行kubectl proxy,然后本地电脑访问 master 的地址上的8001,这样不就可以了吗?

我们来实验一下:
在 master 节点上运行: kubectl proxy –address=’0.0.0.0′ –accept-hosts=’.*’ &

其中–address=’0.0.0.0’是必须的,否则只监听127.0.0.1,这样就不能从外面访问了。–accept-hosts=’.*’表示任何的host都可以访问,否则会报告Forbidden的错误。

然后在本地输入地址:http://masterip:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
,可以出现需要输入token的界面,把token输入后点击登录,登录成功了,但是在config的时候,报错了:MSG_LOGIN_UNAUTHORIZED_ERROR

为什么会这样的呢?于是在master本地访问后,发现在config的时候,会把从login获得的token放入localhost这个域中进行传输,而我输入的是masterip,那么dashboard的界面js就放不进去了。

所以,只有一个办法可以了,必须在本地的机器上运行这个地址才可以的,其他地址都不对:http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ , 那么怎么才能通过localhost访问到 masterip 呢?

嗯,马上想到了,用代理的办法,于是下载一个CCProxy或者nginx作为代理,这样就可以了。

对于网上还有其他的说法是:ssh端口转发,我觉得没有代理来得方便。

tar打包时候的2个注意点

打包的时候千万要注意语句的次序

暂时先不说打包的命令,我们来说一下拷贝的命令,比如将 /tmp/ 下面的所有文件及文件夹拷贝到 /a/ 下面,如下:
cp -r /tmp/* /a/

再来看看,将/tmp/下面的所有文件及文件夹打包成 /tmp/a.tar.gz 文件,命令如下:
tar zcvf /tmp/a.tar.gz /tmp/*

注意这2个语句的次序,cp source dest,而 tar dest source

所以,对于 tar 来说是非常容易弄错的,要是弄错了,重新再来也无所谓,要命的是,tar也不知道是怎么个的脑洞打开,假如次序弄错了,会把 source 给弄丢了,这样会导致严重的问题。

有一次,我就这么弄错了,然后把source 日志文件给弄丢了,犯了一次错误后,我以后每次用 tar 命令,我就会想起那次错误教训,每次都会小心再小心的使用tar命令,那有没有当打错次序后不会把 source 给弄丢的方式方法呢?很可惜,有是有的,但是没有特别好的,比如有人用 alias 来解决这个问题,但是没有用的,比如针对上面的,用 alias 的方法就失效了:tar zcvf /tmp/a.tar.gz /tmp/*

所以,tar的作者在写这个命令的时候是不严谨的,有缺陷的。

打包文件时把隐藏文件加入

tar zcvf /tmp/a.tar.gz /tmp/*, 这样的写法是不包含打包隐藏文件的
tar zcvf /tmp/a.tar.gz .[!.]* *, 需要这样写才可以

如何对nginx的日志进行按天切割

这篇博文分享的技巧非常不错,我转载一下他的博文:https://jingsam.github.io/2019/01/15/nginx-access-log.html

nginx日志分割是很常见的运维工作,关于这方面的文章也很多,通常无外乎两种做法:一是采用cron定期执行shell脚本对日志文件进行归档;二是使用专门日志归档工作logrotate。

第一种写shell脚本的方法用得不多,毕竟太原始。相比之下,使用logrotate则要省心得多,配置logrotate很简单。关于如何配置logrotate不是本文要讲的内容,感兴趣的话可以自行搜索。

虽然大多数Linux发行版都自带了logrotate,但在有些情况下不见得安装了logrotate,比如nginx的docker镜像、较老版本的Linux发行版。虽然我们可以使用包管理器安装logrotate,但前提是服务器能够访问互联网,企业内部的服务器可不一定能够联网。

其实我们有更简单的方法,从nginx 0.7.6版本开始access_log的路径配置可以包含变量,我们可以利用这个特性来实现日志分割。例如,我们想按天来分割日志,那么我们可以这样配置:

access_log logs/access-$logdate.log main;

那么接下来的问题是我们怎么提取出$logdate这个变量?网上有建议使用下面的方法:

if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})") {
  set $year $1;
  set $month $2;
  set $day $3;
}

access_log logs/access-$year-$month-$day.log main;

上面的方法有两个问题:一是如果if条件不成立,那么$year、$month和$month这三个变量将不会被设置,那么日志将会记录到access-$year-$month-$day.log这个文件中;二是if只能出现在server和location块中,而access_log通常会配置到顶层的http块中,这时候if就不适用。

如果要在http块中设置access_log,更好的方法是使用map指令:

map $time_iso8601 $logdate {
  '~^(?<ymd>\d{4}-\d{2}-\d{2})' $ymd;
  default                       'date-not-found';
}

access_log logs/access-$logdate.log main;

map指令通过设置默认值,保证$logdate始终有值,并且可以出现在http块中,完美地解决了if指令的问题。

补充上面的博文一点,因为一般nginx的user是nobody,所以上面写access_log logs/access-$logdate.log main;有可能报错,需要将那个目录所有者授予nobody这个用户。

postman的一些小技巧

分析postman请求的错误原因

在使用postman的时候,有时候会得到这样的响应结果:Could not get any response,然后给出了:
Why this might have happened:
1,The server couldn’t send a response: Ensure that the backend is working properly
2,Self-signed SSL certificates are being blocked: Fix this by turning off ‘SSL certificate verification’ in Settings > General
3,Proxy configured incorrectly: Ensure that proxy is configured correctly in Settings > Proxy
4, Request timeout: Change request timeout in Settings > General

弄得莫名其妙的,也不知道究竟发生了什么错误。

这个时候,需要打开 postman 的左下角有个 console 的图标,打开后可以打开 console 界面,这样每个请求就能看出到底错误在什么地方,而不是上面莫名其妙的错误。

解决服务端的SSL证书错误

有一个错误是这样的:Error: SSL Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE
然后根据他的提示:Self-signed SSL certificates are being blocked: Fix this by turning off ‘SSL certificate verification’ in Settings > General。
需要在postman的右上角,有个扳手一样的图标,点进去后可以设置是否要开启SSL certificate verification。

当然了,通过关闭SSL certificate verification,是可以起效的,但是也说明了服务端的证书是有问题的,最好是修复服务端的证书,而不是通过设置postman的设置。

多用环境变量

当有多个请求都需要某个相同的参数的时候,首先要考虑使用环境变量。这里吐槽一下,这个postman的environments这个名词取得不合适,我认为应该取全局变量更加贴切。因为环境变量很容易和系统的环境变量相混淆。

通过.jks生成nginx所需要的.crt和.key证书

有时候拿到一个证书压缩包,里面包含着各种格式的证书,有.jks的,就是没有.crt和.key证书。

所以,需要将.jks的证书转换成nginx所需要的.crt和.key证书。

比如有一个abc.com.jks

  1. 查看.jks文件中有哪些内容
    keytool -list -keystore abc.com.jks

  2. 将”.jks”转为”.pkcs12”(PKCS12格式的证书库)
    keytool -importkeystore -srckeystore abc.com.jks -destkeystore abc.com.pkcs12 -deststoretype PKCS12

  3. 提取2张证书
    openssl pkcs12 -in abc.com.pkcs12 -nokeys -clcerts -out abc.com-ssl.crt
    openssl pkcs12 -in abc.com.pkcs12 -nokeys -cacerts -out abc.com-ca.crt

abc.com-ssl.crt是SSL证书,abc.com-ca.crt是中级证书,俩个合并到一起才是nginx服务器所需要的证书,这个非常重要。假如仅用一个,尽管在浏览器中可以使用,但是在程序中调用的时候,假如用了validate ssl功能的话,就会报错。

  1. 合并证书
    cat abc.com-ssl.crt abc.com-ca.crt > abc.com.crt
    此时abc.com.crt是一个完成的证书

  2. 提取私钥
    openssl pkcs12 -nocerts -nodes -in abc.com.pkcs12 -out abc.com.key

至此,.crt和.key证书转换完成。

linux cp 强制覆盖不生效的解决办法

在linux中,拷贝并覆盖文件,我们用 cp -f source dest 这个命令,其中 -f 表示就是强制覆盖而不用提示。

但是,你能发现提示是否覆盖还是弹出来了,这是为什么呢?

因为在linux在登录 login shell 的时候,会自动运行 ~/.bashrc 这个文件中的:
alias rm=’rm -i’
alias cp=’cp -i’
alias mv=’mv -i’
所以,仅仅用 cp -f,实际上是 cp -i -f, 而 i 和 f 两者是互斥的,默认linux就选择了 -i。

那么应该怎样才能强制覆盖而不会弹出提示呢?可以用下面的3种方法:
1,unaslias cp (这只是临时取消cp的别名,不是永久的),然后再 cp -f source dest 就可以了。
2,\cp -f source dest ,这个效果就等同于上面,反斜杠表示取消cp的别名。
3,yes|cp -fr source dest,使用管道自动输入yes。这个 yes 特别有意思,假如在控制台中直接输入yes,就可以发现很有趣的现象,不断地输出y。查看一下yes –help,能发现y是可以换成其他字符串的。

zookeeper和curator的版本问题

有一个工程,用到了curator 4.0.1版本,在本地进行调试开发,为了方便,启动了本地的一个低版本的zookeeper,结果在启动的时候报错 zookeeper 连接不上,当时就怀疑了 zookeeper 的版本过低导致。

于是到 apache 官网下载最新版本的 zookeeper,下载了:https://mirrors.bfsu.edu.cn/apache/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz ,然后进行解压,居然说解压失败,有文件重复,以为自己下载的不正确,反复用不同的工具下载了好几遍都是同样的错误。为了解决这个问题,我在linux环境中下载这个包,进行解压,没有报错了,然后再压缩,再下载到本地后解压,没有错了。

启动 zookeeper 之前,需要将 zookeeper 解压后的 conf/zoo_sample.cfg 改名为 zoo.cfg,然后 bin/zkServer.cmd start 启动即可。

然后运行 curator 4.0.1 ,这样就顺利连接上了。

网上的人这么说的:
Curator 存在版本兼容问题。
Curator 2.x.x-兼容两个zk 3.4.x 和zk 3.5.x,
Curator 3.x.x-兼容兼容zk 3.5。

AES加密算法简述

AES是对称性加密算法中最为流行的算法之一。

对于AES加密算法的流程,可以参考这篇博文:AES加密模式与填充
AES有5种模式,其中ECB模式是不安全的,已经废弃;CBC是APPLE默认的加密模式,所以我们也选用CBC来讲解。

我们用java的代码来举例AES算法的一些流程和细节问题:

public static String aesEncrypt(String plainText, String secret,String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes("utf-8"),"AES");

            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes("utf-8"));

            cipher.init(Cipher.ENCRYPT_MODE,keySpec,ivParameterSpec);

            byte[] byteContent = plainText.getBytes("utf-8");
            byte[] result = cipher.doFinal(byteContent);
            String encryptText = Base64.getEncoder().encodeToString(result);
            return encryptText;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

结合上篇博文中的CBC模式图,可以看到:
1,初始化向量(iv)就好比随机数中的种子。
2,PKCS5Padding,因为AES的数据库用128位去切分,当最后的部分不足128位的时候,那么就需要进行填充来达到128位。

其中,AES对secret(密钥)和iv的长度有规定:
1,secret有128位,192位,256位。在java中,128位的AES是jdk自带的,但是大于128位的AES,则需要引入一个额外的security包。从线上运维来看,这可能会增加一定的复杂度,所以,我从个人角度上来看,128位足够,减少运维的复杂度。加密算法的安全在乎与密码是否安全保护好,而不在于是128位还是256位。
2,iv是128位的。
3,我们在输secret的时候,往往不会在乎多少长,比如任意长度的secret,而不会刚好128位,对于iv是同样的道理。那么我们可以采用下面的方式方法来简化这个流程:

String md5 = md5Hex(secret);
secret = md5.substring(0,15);
String iv = md5.substring(16);