Technical Stacks

WebServer / Reverser Proxy

Available Candidates

Name License Extensible Reverse Proxy Load Balance Auto SSL Cloud Native
httpd Apache
Nginx Nginx License Y Y Y
Caddy Apache Y Y Y Y
Traefik MIT Y Y Y
Envoy Apache Y Y

Application Monitors

Available Candidates

Name License Speciality
Zipkin Apache 2.0 Performance monitoring
Elastic APM Apache 2.0 Performance monitoring
Geneva
Geneva

JMeter 使用笔记

安装配置

安装配置

需要安装Java,尽量使用Java SE 8以上的版本。添加把JRE和JDK添加到环境变量中。

JMeter安装

JMeter官网下载相关版本的JMeter链接,直接解压即可。注意要把JMeter的根目录添加到环境变量JMETER_HOME中,之后集群测试会使用到。

单机测试

单机测试主要分为两个大的步骤,一个是添加线程组和Http请求,另一个就是添加Listener来查看测试结果,网络上有很多现成的教程,这里就不再赘述。

集群测试

集群测试很简单,只要把Slave机器部署好,Master会把测试脚本发送给Slave进行测试,部署过程也可以参考这篇文章。具体部署步骤如下:

  1. 在每台Slave上都安装配置好单机环境

  2. 在Slave上修改$JMETER_HOME/bin/jmeter.properties文件,主要修改以下几部分(1099为JMeter服务器默认端口)

    1
    2
    3
    server_port=1099
    server.rmi.localport=1099
    server.rmi.ssl.disable=true
  3. 在Master上修改$JMETER_HOME/bin/jmeter.properties文件,主要修改以下几部分,如果想要在Master上也运行测试程序的话,可以把以上Slave的配置也加到Master里,并且在remote_hosts中添加localhost:1099的项

    1
    2
    # 10.0.0.X为Slave的IP
    remote_hosts=10.0.0.1:1099,10.0.0.2:1099

完成了以上工作,在Master上启动测试程序(运行->远程启动所有)即可开始集群测试。这种模式下有一个非常大的缺点,就是各个Slave的测试结果会实时发送到Master上,对Master的网络和CPU都造成了很大的压力,如果能够积累一定的数据之后批量发送会好很多。也可能是我没有找到正确的使用方法,如果有解决方案,还请不吝告知。

常见问题

Http Post请求该怎么发

Post请求的Body在编程语言层面有很多的叫法,什么Payload、Data、Params、KWargs之类,在JMeter的Http请求界面,选中Method为POST之后,下边的参数栏有三个Tab,分别是Parameters、Body Data以及Files Upload,因为在GET请求时是使用的Parameters,所以这里一上来就往Body Data里填数据。但是当Content-Type为multipart/form-data,并且同时发送参数和文件的时候,在Body Data里填写的参数会被Files Upload的内容覆盖掉。所以至少在这种情况下需要把请求体中的参数填到Parameters栏里。

在其他Content-Type中尚未见到这种限制,比如application/json时就可以直接把Json对象填入到Body Data中。

表单请求无法正常发送

当Content-Type为multipart/form-data,并且同时发送参数和文件的时候,可能请求无法成功发出去,如果其他工具(比如Postman)可以正常发送请求,那么应该是JMeter实现的问题,在Http请求的Advanced标签中把Client implementation换成Java即可解决这个问题。

集群运行时报找不到rmi_keystore.jks错误

因为JMeter集群在运行时默认会开启SSL,所以需要额外进行SSL的配置,如果想要跳过SSL直接使用,需要在jmeter.properties中设置以下内容

1
server.rmi.ssl.disable=true

Python异步编程

异步编程

在Python中,由于CPythonGIL限制,不能使用多线程充分利用资源,因此在进行诸如文件存取、网络请求等IO操作的时候极其浪费资源,程序往往要在一个点上空等。虽然Python可以借助多进程来改善,但是进程相比线程来说过重,如果只用多进程就可以完全解决问题,线程这个概念也就不会出现了。

在耗时任务(主要是IO)的操作上,Python提供了一些方法来解决,比如协程的概念。初次了解协程的时候认为这是可以拯救世界的东西,概念新颖,方法独特,但是在了解了如C#的async/await以及Javascript(同样是单线程语言)的async/await之后,明白了基于yield/send的协程仍然使用起来仍然不够顺手。

Python3.4版本之后,Python引入了asyncio标准库,使用@coroutineEventLoop可以更方便地完成Python协程的工作,接着在Python3.5中,Python引入了async/await关键字,彻底简化了Python异步编程流程。自从C#最早提出async/await之后,很多语言都引入了这一机制,因为它真的太好用了,可以完全以同步的风格进行异步编程。不过由于与C#的实现机制不同,在Python协程中仍然有一些具体的细节无法回避,比如send/yield的交互。

工具对比

在Python生态系统中,有很多异步编程库可以使用,这些编程库有的是诞生于Python异步支持之前,自行实现了事件循环,有的是依托于Python的异步机制进一步开发,在进行异步编程的时候,可以借助以下的工具来简化工作:

  • gevent:自己实现了时间循环,很早的异步处理库
  • twisted:非常早的异步网络库,自带HTTP服务器、DNS服务器、邮件服务器等,之后Python官方的asyncio就很大地收到了它的影响
  • tornado:既是异步网络编程库,也是一个成熟的HTTP服务器以及Web框架
  • aiohttp:基于asyncio的网络编程库,可以高效地实现HTTP服务器和Websocket服务器
  • cyclone:作者想要综合twistedtornado两个库,做到implements the Tornado API as a Twisted protocol

分布式平台搭建

搭建Hadoop

以下操作皆基于Ubuntu 16.04 32位环境 & Hadoop 2.7.3版本

单节点环境

  • 首先安装相关的准备环境

    1
    2
    3
    4
    $ sudo apt update
    $ sudo apt install -y default-jdk
    $ sudo apt install -y vim
    $ sudo apt install -y openssh-server
  • 配置JAVA_HOME

    • 一般来说Ubuntu平台上通过default-jdk安装的java位置在/usr/lib/jvm/default-jdk目录
    • /etc/profile中添加下边两行内容并保存退出
      1
      2
      export JAVA_HOME=/usr/lib/jvm/default-java
      export PATH=$PATH:$JAVA_HOME/bin
  • 添加用户和用户组

    1
    2
    3
    $ sudo addgroup hadoop
    $ sudo adduser --ingroup hadoop hduser
    $ sudo usermod -a -G sudo hduser
  • 配置 SSH

    • 切换到hduser,并执行以下操作

      1
      2
      $ ssh-keygen -t rsa -P ""
      $ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
    • 之后尝试使用下边的命令连接本机,这个操作也会将本机加入到known hosts里

      1
      $ ssh localhost
  • 安装Hadoop

    • 下载hadoop-2.7.3.tar.gz,以下假定工作目录为/home/hduser

      1
      2
      3
      $ tar -zxvf hadoop-2.7.3.tar.gz
      $ mv hadoop-2.7.3/ hadoop/
      $ sudo chown -R hduser:hadoop hadoop
  • 编辑bash配置

    • 打开.bashrc,加入以下内容(也可以修改/etc/profile,但是使用.bashrc更加灵活)

      1
      2
      3
      export JAVA_HOME=/usr/lib/jvm/default-java
      export HADOOP_HOME=/home/hduser/hadoop
      export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin
  • 配置Hadoop(不同版本的hadoop的配置方案会有不同,配置之前记得要确定版本)

    • 修改~/hadoop/etc/hadoop/hadoop-env.sh,在文件中添加下边的内容

      1
      export JAVA_HOME=/usr/lib/jvm/default-java
    • 修改~/hadoop/etc/hadoop/yarn-env.sh,在文件中添加下边的内容

      1
      export JAVA_HOME=/usr/lib/jvm/default-java
    • 修改~/hadoop/etc/hadoop/core-site.xml

      • 执行以下命令

        1
        2
        3
        $ sudo mkdir -p /home/hduser/tmp
        $ sudo chown hduser:hadoop /home/hduser/tmp
        $ sudo chmod 750 /home/hduser/tmp
      • 在core-site.xml添加以下内容(各个属性的内容可以按照实际情况修改)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        <configuration>
        <property>
        <name>fs.defaultFS</name>
        <value>hdfs://master:9000</value>
        </property>
        <property>
        <name>io.file.buffer.size</name>
        <value>131072</value>
        </property>
        <property>
        <name>hadoop.tmp.dir</name>
        <value>file:/home/hduser/tmp</value>
        <description>Abase for other temporary directories.</description>
        </property>
        <property>
        <name>hadoop.proxyuser.hduser.hosts</name>
        <value>*</value>
        </property>
        <property>
        <name>hadoop.proxyuser.hduser.groups</name>
        <value>*</value>
        </property>
        </configuration>
    • 修改~/hadoop/etc/hadoop/hdfs-site.xml,添加以下内容(各个属性的内容可以按照实际情况修改)

      • 执行以下命令

        1
        2
        3
        4
        5
        6
        $ sudo mkdir -p /home/hduser/dfs/name
        $ sudo chown hduser:hadoop /home/hduser/dfs/name
        $ sudo chmod 750 /home/hduser/dfs/name
        $ sudo mkdir -p /home/hduser/dfs/data
        $ sudo chown hduser:hadoop /home/hduser/dfs/data
        $ sudo chmod 750 /home/hduser/dfs/data
      • 在hdfs-site.xml中添加以下内容

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        <configuration>
        <property>
        <name>dfs.namenode.secondary.http-address</name>
        <value>master:9001</value>
        </property>
        <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:/home/hduser/dfs/name</value>
        </property>
        <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:/home/hduser/dfs/data</value>
        </property>
        <property>
        <name>dfs.replication</name>
        <value>3</value>
        </property>
        <property>
        <name>dfs.webhdfs.enabled</name>
        <value>true</value>
        </property>
        </configuration>
    • 修改~/hadoop/etc/hadoop/mapred-site.xml,添加以下内容(各个属性的内容可以按照实际情况修改)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <configuration>
      <property>
      <name>mapreduce.framework.name</name>
      <value>yarn</value>
      </property>
      <property>
      <name>mapreduce.jobhistory.address</name>
      <value>master:10020</value>
      </property>
      <property>
      <name>mapreduce.jobhistory.webapp.address</name>
      <value>master:19888</value>
      </property>
      </configuration>
    • 修改~/hadoop/etc/hadoop/yarn-site.xml(各个属性的内容可以按照实际情况修改)

      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
      <configuration>
      <property>
      <name>yarn.nodemanager.aux-services</name>
      <value>mapreduce_shuffle</value>
      </property>
      <property>
      <name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name>
      <value>org.apache.hadoop.mapred.ShuffleHandler</value>
      </property>
      <property>
      <name>yarn.resourcemanager.address</name>
      <value> master:8032</value>
      </property>
      <property>
      <name>yarn.resourcemanager.scheduler.address</name>
      <value> master:8030</value>
      </property>
      <property>
      <name>yarn.resourcemanager.resource-tracker.address</name>
      <value> master:8031</value>
      </property>
      <property>
      <name>yarn.resourcemanager.admin.address</name>
      <value> master:8033</value>
      </property>
      <property>
      <name>yarn.resourcemanager.webapp.address</name>
      <value> master:8088</value>
      </property>
      </configuration>
  • 格式化Namenode

    • 进入~/hadoop/bin目录下,执行以下操作

      1
      $ ./hdfs namenode –format
  • 启动hadoop

    • ~/hadoop/sbin执行以下命令

      1
      2
      $ ./start-dfs.sh
      $ ./start-yarn.sh
    • 之后通过jps命令可以看到以下结果,说明启动成功

      1
      2
      3
      4
      13058	Jps
      13026 NodeManager
      12916 ResourceManager
      12169 DataNode

集群环境

此处默认master和slave已经按照单机节点配置完成

  • 设置网络

    • 我的例子是一台master一台slave,分别确定两者的ip,然后在两台机器的/etc/hosts中写入以下内容

      1
      2
      192.168.0.1    master
      192.168.0.2 slave
  • 配置ssh免密登录

    • 在master上执行以下命令

      1
      $ ssh-copy-id -i ~/.ssh/id_rsa.pub hduser@slave
    • 在slave上执行以下命令

      1
      $ ssh-copy-id -i ~/.ssh/id_rsa.pub hduser@master
    • 之后两方互相登录测试一下是否可以免密登录

  • 配置~/hadoop/etc/hadoop/slaves

    • 在master机器的slaves文件中添加以下内容

      1
      2
      master
      slave
  • 格式化Namenode

    • 进入master的~/hadoop/bin目录下,执行以下操作

      1
      $ ./hdfs namenode –format
  • 启动hadoop

    • 在master的~/hadoop/sbin执行以下命令

      1
      2
      $ ./start-dfs.sh
      $ ./start-yarn.sh
    • 之后通过jps命令可以看到以下结果,说明启动成功

      • master中

        1
        2
        3
        4
        5
        6
        4048	Jps
        3899 ResourceManager
        4013 NodeManager
        3726 SecondaryNameNode
        3423 NameNode
        3535 DataNode
      • slave中

        1
        2
        3
        2754	DataNode
        3012 Jps
        2903 NodeManager

搭建Spark

Spark平台

  • 下载Spark

    • 这里下载和hadoop版本对应的spark,假定工作目录为/home/hduser

    • 进行如下操作解压压缩包(每个节点都要进行)

      1
      $ tar -zxvf spark-2.3.1-bin-hadoop2.7.tgz
  • 配置~/.bashrc文件

    • 在.bashrc文件中添加以下内容(每个节点都要添加)

      1
      2
      export SPARK_HOME=/home/hduser/spark
      export PATH=$PATH:$SPARK_HOME/bin
  • 配置~/spark/conf/slaves文件

    • 在两个节点的salves文件中添加以下内容

      1
      2
      master
      slave
  • 启动Spark

    • 在master的~/spark/sbin中执行以下命令

      1
      ./start-all.sh
    • 在master中执行jps,发现以下结果说明spark运行成功

      1
      2
      3
      4
      5
      6
      5257	Worker
      5177 Master
      3726 SecondaryNameNode
      5326 Jps
      3432 NameNode
      3535 DataNode
    • 在slave中执行jsp,发现一下结果说明spark运行成功

      1
      2
      3
      2754	DataNode
      3717 Worker
      3771 Jps

配置Scala

  • 下载Scala

    • 这里相应版本的scala压缩包,假定工作目录为/home/hduser

    • 进行如下操作解压压缩包(只需要在master进行)

      1
      $ tar -zxvf scala-2.12.6.tgz
  • 配置~/.bashrc文件

    • 在.bashrc文件中添加以下内容(只需要在master在master上执行以下命令)

      1
      2
      export SCALA_HOME=/home/hduser/scala
      export PATH=$PATH:$SCALA_HOME/bin

配置 Eclipse

  • 安装Eclipse

    • 在master上执行以下命令

      1
      $ sudo apt install eclipse-platform
  • 安装Eclipse插件

参考资料

  1. 在Ubuntu上搭建单节点Hadoop
  2. 在Ubuntu上搭建Hadoop集群

Android 使用 RecyclerView 分隔线问题

自从Google推出了RecyclerView作为ListView功能更加强大的替代品之后,越来越多的APP开始使用这个控件。RecyclerView可以优化程序性能,并且由于功能上的高度解耦,可以无成本地在ListView、GridView以及瀑布流之间切换,但是也正因为如此,原来在ListView中理所当然的东西在RecyclerView中要自己来实现,比如分隔线,比如OnItemClickListener等(不是特别理解为什么Listener还要我们自己实现,我的做法就是往Adapter里扔回调)

说到分隔线,可以去实现RecyclerView.ItemDecoration来定制,在鸿洋大大的博客里有非常详细的讲解,按照这个来做其实也不麻烦,而且一次编写复用终生,但是我做的时候还是想偷懒,不想去写那么多代码,所以想用背景图片来实现分隔线效果。

我的RecyclerView中每个item的背景是这样的,用了selector实现点击前后的变色效果

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/list_item_bg_pressed" android:state_pressed="true"/>
<item android:drawable="@drawable/list_item_bg_unpressed" android:state_pressed="false"/>
</selector>

其中@drawable/list_item_bg_pressed是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 第一层,露出个小边当分隔线 -->
<item>
<shape android:shape="rectangle">
<solid android:color="@color/color_list_item_divider"/>
<padding android:bottom="@dimen/dimen_list_item_divider_height"/>
</shape>
</item>
<!-- 第二层,真正的背景色 -->
<item>
<shape android:shape="rectangle">
<solid android:color="@color/color_list_item_bg_pressed"/>
</shape>
</item>
</layer-list>

使用layer-list来实现效果:第一层使用分割线的颜色,并在底部padding一个窄边,第二层使用正常背景颜色,这样写而不是直接用一张图片代替的原因是:如果使用普通图片作为背景,当图片被拉伸时分隔线会明显变粗,效果非常不美观(当然.9图片没有问题,想了想还是直接用代码写了),而用代码就没有这个问题。这种效果我还用来实现一些输入框。

@drawable/list_item_bg_unpressed 同理,就是把第二层的颜色换了一下。

那么为什么不直接用一张图片来代替呢?因为有些情况下RecyclerView每个条目的高度并不是一样的,这样对于普通图片会产生纵向拉伸的效果,使得分隔线变得非常可怕,而用代码来实现的.9效果则不会有这个问题。

javax.el.PropertyNotFoundException: Property 'XXX' not readable

使用spring框架,自己组装了一个信息搭载类扔到JSP中渲染,大致是这样:

1
UserBasicInfo userInfo = new UserBasicInfo(user);

以及

1
<img id="user_avatar" src="${userInfo.avatar}" class="img-responsive col-md-6" alt="用户头像"/>

这时候运行时就会出现标题中的错误,UserBasicInfo是pojo,是放在UserController.java中的一个非public类。试过把他做成bean仍会有这个错误,之后考虑spring是使用代理来做这些事情的,默认访问权限的类spring可能访问不到,就把这个类单独做成一个文件做成public的,问题解决。

Hibernate com.mysql.jdbc.exceptions.MySQLSyntaxErrorException MYSQL

大作业,使用SpringMVC + Hibernate搭一个小网站,但是中途出现了这个问题,困扰了我一天时间。

是的这个问题很烦人,所以如果你已经尝试过:

  • 查找StackOverflow
  • 多次更换数据库方言
  • 更换数据库驱动
  • 你甚至丧心病狂地更换数据库

仍然没有结果的话,那么你的问题应该就是

你丫是不是把数据库保留字当列名了,真以为SQL不是编程语言啊

血泪的教训,以上

Hibernate 插入中文乱码问题

最近在做JSP大作业,因为懒得写jdbc + 封装字段了,再加上也想学习一下SSH,就使用Hibernate来做持久化。但是中间遇到了中文乱码问题:

向数据库插入中文之后,在sqlbuddy看到的结果是乱码,已经确定乱码问题一定出在Hibernate向数据库插入的过程中。然而明明已经在前端、Servlet、数据库分别设置了字符编码是UTF-8,仍然出现乱码问题。找了很多解决方案终于了解到:Hibernate自己的字符编码默认不是UTF-8,所以要在hibernate.cfg.xml中的connection.url设置如下内容:

1
<property name="connection.url">jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&amp;characterEncoding=UTF-8</property>

设置完成之后,可以正常进行中文操作。

JavaScript 中的运算操作陷阱

最近做JSP大作业,需要写一个网站,其中用到了分页系统,就自己撸了一个。

其中的翻页逻辑是这样写的:

1
2
3
4
5
6
7
8
/*上一页*/
previousPage.click(function(){
clickPage(grids[currentActivePos].find('a').html() - 1);
});
/*下一页*/
nextPage.click(function() {
clickPage(grids[currentActivePos].find('a').html() + 1);
});

扔到网页里实验,发现[上一页]功能没有问题,但是点击[下一页]之后没有反应,下边还有一个类似的错误:

1
grids[i].find('a').html(index + i - 3);

当测试时,index是从.html()方法获取的字面值为3的参数,i是循环控制变量,当i也是3时,出现了gridsp[i]的值变成30而不是3的情况。

一直想不明白这奇怪的现象是因为什么,但问题肯定就出在这句话上,所以就试着把表达式的顺序变了一下,像这样:

1
grids[i].find('a').html(i - 3 + index);

发现结果变成了03,到这里才明白这结果不是数值而是字符串!也就是index是string类型的,直接用数字和它进行+操作结果不是进行数值运算而是字符串连接!

因而也明白了翻页逻辑的错误在哪里。改动如下:

1
2
3
4
5
6
7
8
9
10
11
/*上一页*/
previousPage.click(function(){
clickPage(grids[currentActivePos].find('a').html() - 1);
});
/*下一页*/
nextPage.click(function() {
var des = eval(grids[currentActivePos].find('a').html() + '+ i');
clickPage(des);
});
/*先进行减法运算,把表达式结果转换成数值类型*/
grids[i].find('a').html(index - 3 + i);

总结:非常傻的一个错误,找到原因之后觉得自己非常基础的东西没有掌握牢固。这一段时间虽然涉及的面不少,但是都没有什么深入,这样就有点像”非科班”的CS学生了,应该警醒。

Android ListView 和 ScrollView 冲突问题

最近做一款APP,其中有一个类似微博的评论功能的界面,先是列出微博的正文内容和图片等,然后下边是评论。一开始就想着用一个ScrollView把主要内容和评论区的ListView包起来,然后添加各个控件的内容即可(对,感觉上有点像这个CSDN博客的编辑界面嘛),但是写出来之后发现ListView只显示出了一个条目的高度,并且不能滑动,网上搜了一下发现原因是ScrollView和ListView都是可滑动的,把它们放在一块会有冲突,最后还是ScrollView获得了焦点,ListView不能滑动。网上的解决方法最多的是在加载ListView时用getMeasure计算每个条目和分割线的高度,然后相加,把结果设置为ListView控件的高度,不过貌似是只适用于ListView每个条目高度都一样的情况(没有试过,很奇怪为什么会这样)。要么就是自定义一个继承自ListView的控件,也是事先设置好ListView的高度,但这样总归比较麻烦,而且准确度不如由系统自己构造好。

懒癌发作实在不想自己去做这些事情,于是便想试一下比较投机的方法,就是在ListView的Adapter的getView方法中根据position构造不同的界面,即如果position是0,则用原来主要信息(微博正文,图片)的xml文件取inflate convertView,否则就用评论条目的xml去inflate,经试验果然可行。之后不死心想看下有没有更好的实现方法,去overflow上找了一下,发现有人推荐的方法和我的差不多,所以认为这种方法是比较好的,不需要做额外的工作,只需要把inflate的工作由主Activity放在Adapter里就可以了。

getView方法

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
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
/*主信息界面*/
if(0 == position)
{
MainHolder holder = null;
convertView = inflater.inflate(R.layout.info, parent, false);
holder = new MainHolder();
convertView.setTag(holder);
······
······
}
/*评论界面*/
else
{
ItemHolder holder = null;
convertView = inflater.inflate(R.layout.item, parent, false);
holder = new ItemHolder();
convertView.setTag(holder);
······
······
return convertView;
}
}