Layout's Studying Notes

My studying notes for Java,Ruby,Ajax and other any interesting things.

星期二, 八月 01, 2017

mysql性能问题排查及故障诊断思路


思路:
——————————————————————— 
1.找到瓶颈处
2.针对瓶颈进行优化
3.数据库优化策略分为三步走:
  • 配置优化,cpu,内存,io
  • 表结构优化:表引擎,索引,表拆分(大表转小表,减少大字段,控制表数量及表大小),分表分库,主从分离,读写分离,多主
  • SQL优化:简化SQL,减少join,减少sql计算,减少存储过程,使用preparedstatement,找到慢sql逐个优化
4.程序优化:
  • 减少事务,降低锁的概率
  • 使用缓存应对读(但要保证数据的及时性)
  • 异步写操作(但要保证事务顺序和成功)


实践:
——————————————————————— 
寻找瓶颈,有两个思路:
  • 根据监控找到耗时较大的功能、业务:
    • 程序级别的监控,每个请求的时间记录,access_log或者程序在某个controller的切面中将时间打印出来;
    • 根据用户反馈,这个往往不准,因为用户看到慢的时候可能已经形成了雪崩效应;
    • 捋日志,找第一个慢的,或者找到累计最多的,然后单独测试该功能复现和模拟;
    • 原因可能会很多:比如数据库锁住了,内存不够,SQL性能太低....根据原因找;
  • 根据性能监控,查看资源使用情况
    • 在应用服务器,数据库服务器多方面监控资源使用情况,核心关注CPU,内存,IO和网络三点的使用情况
    • CPU忙:
      • 通常在进行计算,计算包括数据处理,图像,正则,科学计算等;从直观上来看,就是循环多,有很多for循环..
      • 常用监控命令:vmstat,mpstat
      • 如果要看某个进程的情况,可以使用"while :; do ps -eo pid,ni,pri,pcpu,psr,comm | grep 'firefox'; sleep 1; done" 类似的语句来查看
    • IO忙:大数据load到内存中,比如数据库、文件检索,本身匹配并不耗CPU,但是数据比较大,比较多需要频繁换页;
      • 监控换页数量,io读写次数,以及iowait数量
      • 常用命令:iostat,lsof
      • 需要注意,swap其实挺慢的,可以通过top、cat /proc/meminfo、vmstat 来监控swap的使用情况;
    • 网络监控:
      • 监控起来最复杂,主要监控网络流出和流入情况,以及丢包、延迟等;
      • 在linux下面可以使用:iptraf,netperf,tcpdump,tcptrace,tcpcopy
  • 工具:salt+zabbix,negios等都可以实现细粒度的监控;
  • 压力测试:针对某些怀疑的点进行压力测试,模拟发现对应的问题,同时检查系统的吞吐能力;
  • 常用工具及命令:Vmstat、sar、iostat、netstat、free、ps、top等
  • 常用组合方式
    • 用vmstat、sar、iostat检测是否是CPU瓶颈
    • 用free、vmstat检测是否是内存瓶颈
    • 用iostat检测是否是磁盘I/O瓶颈
    • 用netstat检测是否是网络带宽瓶颈

针对瓶颈优化:
  • 不同的问题原因不同,优化方案也不一样,通常来讲,分为代码优化和配置优化两条腿,配置优化又分为操作系统,服务和应用的优化。

mysql配置优化:
————————— 
  • 安装以后在mysql的配置文件中有:my-huge.cnf,my-mediam.cnf,my-small.cnf,可以根据需要自己拷贝一份做修改;/usr/local/share/mysql/
  • innodb_buffer_pool_size:数据缓冲池,是数据和索引缓存的地方,这个值越大越好,这能保证你在大多数的读取操作时使用的是内存而不是硬盘。典型的值是5-6GB(8GB内存),20-25GB(32GB内存),100-120GB(128GB内存);根据内存情况来设定。
  • innodb_log_file_size:redo日志的大小,用于确保写操作快速可靠,且在崩溃的时候可以恢复;默认512M,4G比较合适;
  • max_connections:最大连接数,默认151,根据实际情况扩大,但要注意程序端不会无限的增加(连接池)
  • innodb专属设置:
    • innodb_file_per_table,建议为on,每个表单独有自己的idb文件,可以再drop,truncate或者delete的时候会收表空间,如果表特别多的话,不建议使用(比如超过10k)
    • innodb_flush_log_at_trx_commit,是否支持事务一致性,通常为1,当性能要求大于数据一致性要求是可以设为0,比如从库;
    • innodb_flush_method,数据和日志写入方式,如果有raid,且不会down机,使用O_Driect,否则用fdatasync,可以用sysbench来测试;
    • inodb_log_buffer_size,为未执行的事务分配的缓存,默认1M,可以根据innodb_log_waits状态来看,如果不够的话可以适当增加;
  • 其他设置:
    • query_cache_size,查询缓存,这个可能会有坑,建议设为0,通过其他方式来提升缓存;
    • 设定主从:log_bin,
    • 是否跳过域名解析:skip_name_resolve,这样数据中只能使用ip来进行授权;
    • table_cache,表缓存,可以根据open_tables(show global status)来进行优化

mysql性能诊断命令:
  • show global status;show status like 'xxx';
  • show variables
  • show innodb status
  • show processlist
  • show tables status
  • show processlist
  • 事务和锁的检查
mysql的慢查询开启:
  • slow_query_log,设置为on
  • long_query_time,超时时间,比如设置为1,就是1s
  • slow_query_log_file,记录慢日志的文件
  • log_queries_not_using_indexes,记录没有使用到索引的查询语句

根据某个语句进行优化:
  • desc(explain) 某条语句,查看执行计划,可以看到该语句是否使用了索引,临时表、文件排序检索行数等;
  • 设置profiling,然后执行语句,查看profiling的日志:set profiling=1;execute sql;show profiles;show profile for query 1;


mysql结构优化:
————————— 
  • 每个表有主键,
  • 字段简单:
    • 类型选择好,
      • 数字:尽量用整型,包括枚举、时间、ip等都可以数字化,decimal少用
      • 文本:varchar/char,禁用text
      • 时间:性能考虑可以用bigint,否则用timestamp(不用datetime)
    • 二进制、文本、图片等不要用数据库存储,大字段可以分开表存放,减少io;
    • 字符集编码统一,join的时候可能会出现字符集不匹配无法使用索引的问题;
    • 尽量not null
    • 可以适当冗余字段,减少join
    • 拆表拆库:主要将资源分配到更多cpu和内存上,对结构简化有帮助
    • 单表不超过:1000w,有字符串的尽量在500w以内,单库不超过400张表;字段在30-50个;
    • 每个表都有主键,且建议为数据库自增int
使用语句优化:
————————— 
  • 原则:每个sql尽量小,尽量不用大事务,
  • 尽量每条语句都要是用到索引
  • 语句中尽量少join
  • 在语句中少使用子查询,如果一定要,用join比子查询好;
  • 不用函数或在SQL中进行计算,包括类型转换
  • 少用or
  • union all 代替union
  • 少select *,而是需要什么查询什么
  • 尽早过滤
  • 尽量少排序
  • 优先优化数量多,性能没那么好的SQL;再优化频率低,杀伤力大的sql
  • 大量数据插入用load data,且放到低峰期执行
索引创建合理:
  • 索引数量不是越多越好,而是用得越多越好
  • 索引字段不要在sql中计算,可能无法使用索引
  • 尽量不用外键等约束,由程序来保障,避免高并发死锁

程序优化:
——————————————————— 
1.控制事务大小
2.充分利用应用服务器的资源
3.在应用端缓存,分布式、文件、本地都可以




Refer:

星期一, 七月 31, 2017

tmux使用简单记录

简单研究tmux的使用,类似于screen,不过有分屏功能:

有个详细的文章: tmux终端复用


大体有几个关键:
1.安装,mac下面brew install tmux,linux下面用yum或者apt-get都可以正常安装
2.基本使用:

创建和连接session:
  tmux new -s session_name
  tmux a -t session_name
  ctrl+b d, detache一个session

创建新的panel:
  ctrl+b %,纵向拆分一个面板
  ctrl+b " 横向拆分一个面板
  如果有鼠标模式,可以鼠标点击进入不同的panel;
  如果没有鼠标,可以使用ctrl+b 方向键,来切换不同的panel


创建新窗口:
  ctrl+b c 创建一个新的窗口
  ctrl+b n,p 进入上一个或者下一个窗口


3.配置:
在~/.tmux.conf中编辑相关配置,可以设置相关的键盘绑定,基本上默认即可,此处有个简单参考 : http://www.opstool.com/article/253


这里有个文章说如何打造更好的tmux配置: https://segmentfault.com/a/1190000008188987

tmux使用简单记录

一直使用screen,但是在远程的时候开多个窗口比较麻烦,用iterm2的cmd+d拆分也可以实现,但会产生多个ssh链接。无意中发现tmux,试用了一下感觉不错,简单记录。

tmux使用简单记录

简单研究tmux的使用,类似于screen,不过有分屏功能:

有个详细的文章: tmux终端复用


大体有几个关键:
1.安装,mac下面brew install tmux,linux下面用yum或者apt-get都可以正常安装
2.基本使用:

创建和连接session:
  tmux new -s session_name
  tmux a -t session_name
  ctrl+b d, detache一个session

创建新的panel:
  ctrl+b %,纵向拆分一个面板
  ctrl+b " 横向拆分一个面板
  如果有鼠标模式,可以鼠标点击进入不同的panel;
  如果没有鼠标,可以使用ctrl+b 方向键,来切换不同的panel


创建新窗口:
  ctrl+b c 创建一个新的窗口
  ctrl+b n,p 进入上一个或者下一个窗口


3.配置:
在~/.tmux.conf中编辑相关配置,可以设置相关的键盘绑定,基本上默认即可,此处有个简单参考 : http://www.opstool.com/article/253


这里有个文章说如何打造更好的tmux配置: https://segmentfault.com/a/1190000008188987
印象笔记,让记忆永存。下载印象笔记

星期二, 五月 16, 2017

windows下imagemagick水印处理脚本



近期在使用imagemagick处理水印,mac和linux下面都很实用,一个同事是windows系统,也需要类似功能。于是做了一个windows下面的脚本,同时学习了一下windows的bat处理脚本。(真心不好用)

大概记录一下脚本和流程:

— 脚本1:watermark.bat,负责调度和创建目录
echo off
SET IDTY="C:\Program Files\ImageMagick-7.0.5-Q16\identify.exe"
SET SOURCE_DIR="source"
SET LOGO="C:\windows.logo\logo.png"
SET TARGET_DIR="out"

%IDTY% -format "%%[fx:w] %%[fx:h]" %LOGO% > logo.txt

for /f "tokens=1,2" %%a in (logo.txt) do set lw=%%a&set lh=%%b


for /r %SOURCE_DIR% %%i in (*) do if not exist %TARGET_DIR%%%~pi mkdir %TARGET_DIR%%%~pi

for /r %SOURCE_DIR% %%i in (*) do (
      call logo.bat %%i
)

— logo.bat,负责进行logo处理和输出:

SET TARGET_DIR="out"
SET IMCONV="C:\Program Files\ImageMagick-7.0.5-Q16\convert.exe"
SET IDTY="C:\Program Files\ImageMagick-7.0.5-Q16\identify.exe"
SET SOURCE_DIR="source"
SET TARGET_DIR="out"
SET LOGO="C:\Users\zhangwenhui\Desktop\windows.logo++\logo.png"

for /f "tokens=1,2" %%a in (logo.txt) do set lw=%%a&set lh=%%b

@rem echo %lw% %lh%

%IDTY% -format "%%[fx:w] %%[fx:h]" %1 > %1.txt
for /f "tokens=1,2" %%a in (%1.txt) do set w=%%a&set h=%%b
del %1.txt
set /a width=%w%*30/100
set /a height=%lh%*%width%/%lw%
@rem echo w:%w% ,width:%width%,height:%height%

composite -gravity southeast ( %LOGO% -resize %width%x%height% ) %1 %TARGET_DIR%\%~pn1.jpg


有几点注意:
1.windows的bat脚本中的%(百分号)是个坑:
  case1:
    set xx='asdfbc'
    引用xx的时候使用%xx%
   case2:
    在命令行中 for /r /d %d in(*)  do echo %d
    在for里面使用的时候需要用%d的形式访问,然后在后续使用该变量的时候可以使用%d进行访问
  case3:
    在批处理脚本中的for,需要加两个百分号
    for /r /d %%d  in (*) do echo %%d
      如果在for中再进行set xx,就基本上搞不正确了,所以使用了call来调用另外一个脚本;

    2.批处理的执行顺序,在windows的bat处理的时候,感觉会类似递归的调用顺序,没有搞太明白。比如 block a中的block b,在执行的时候是block a把每个block单独输出到命令行被执行,而不是有一个解释器把整个脚本进行调度。
    3.bat中有替换功能,但是好像只能针对%a%的变量,而不能针对%a这样的变量
    4.imagemagick在windows上面默认不安装legacy utilities,所以convert,composite等功能都无法调用,在安装的时候需要选中;在composite的时候与mac略有不同,不需要使用转义括号。
    5.目录中如果有空格,则需要使用引号给引起来;

    印象笔记,让记忆永存。下载印象笔记

    星期三, 十二月 14, 2016

    shell中的if判断

    #!/bin/sh        myPath="/var/log/httpd/"    myFile="/var /log/httpd/access.log"        #这里的-x 参数判断$myPath是否存在并且是否具有可执行权限    if [ ! -x "$myPath"]; then      mkdir "$myPath"    fi    

    #这里的
    -d 参数判断$myPath是否存在  if [ ! -d "$myPath"]; then    mkdir "$myPath"  fi    #这里的-f参数判断$myFile是否存在  if [ ! -f "$myFile" ]; then    touch "$myFile"  fi    #其他参数还有-n,-n是判断一个变量是否是否有值  if [ ! -n "$myVar" ]; then    echo "$myVar is empty"    exit 0  fi    #两个变量判断是否相等  if [ "$var1" = "$var2" ]; then    echo '$var1 eq $var2'  else    echo '$var1 not eq $var2'  fi 


    印象笔记,让记忆永存。下载印象笔记

    星期四, 十二月 01, 2016

    java8的Stream API

    java8中增加了Stream API,在coding过程中经常使用到,每次用的时候总觉的有些新意,没有系统的学习和研究一下。

    今天抽时间,研究一下看看stream api的具体功能和用法:


    总览:
    • stream是java8的一个心功能,这个流与java.io中的流的概念不同,而是对Collection集合类的一个增强,专注与如何高效和便利的处理java集合(包括list,set,map...)
    • stream在处理的时候利用到了新的Lambda表达式(函数式),链式调用,实现了计算时加载,可以处理更大量的数据,同时代码的可读性增强,代码块变小;
    • 同时也支持并行和串行的两种汇聚操作模式,可以充分利用CPU多核优势;

    集合的操作:

    通常集合的操作有这样几种:遍历,过滤, 排序,group by,求最大(最小),求均值等操作。在java7及以前的代码,常用的做法是循环这个集合,获取到集合中的具体元素,针对集合元素进行处理和操作。
    eg:
    for(Type item: items){
       item.doSth();
       if(item.suitable){
          dosth();sort();
       }
    }


    在java8中可以这样写:

    items.stream().filter(item->item.suitable()).map(Item::getXXX).collect(toList());

    相比java7的代码来的更加简洁易懂;但是引入了好几个函数,比如stream(),filter(),map(),collect() 等方法,需要详细了解下其用处。

    详解Stream:

    Stream有点类似于Iterator,是单向的,不可以往复,数据只能遍历一次,遍历一次就用完了,无法再使用。同时Stream支持并行运算,这个比Iterator要更高效。并行API从java.1.4就开始引入(Thread),经历了concurent,phasers,fork/join等方法的发展以后,到了java8引入了Lambda,对stream起了很大的推进作用。

    一个流的组成:
    一个流通常分为三个过程,如下图:获取数据 —> 数据转换 —> 执行操作,每次转换的时候Stream对象不改变,但是会返回一个新的Stream对象,就可以向链式调用一样进行多次转换或者操作。

    生成流有几种办法:

    从Collection或者数组中来:
      Collection.stream();
      Collection.parallelStream()
      Arrays.stream(T array)   Stream.of()(java8)
      java.io.BufferedReader().lines
    静态生成方法:
      java.uti.stream.IntStream.range()
      java.nio.file.Files.walk()
    自己构建:
      java.util.Spliterator
      …



    Stream的操作分为两种类型:
    - Intermediate,一个流可以做零个或者多个这样的操作,目的是打开流,并进行数据的映射,过滤等操作,然后返回一个新的流;这些方法都是惰性(lazy)的,并不会真正的进行操作,只有进行一个Terminal操作的时候,才会被执行。
    - Terminal , 一个流进行了Terminal操作,就被使用光了,无法再被操作。所以,这个操作是流的最后一个操作。做这个操作的时候,所有的Intermediate的操作也才会真正的发生,并产生一个结果。

    还有一种:short-circurting操作,针对intermediate和terminal操作意义不同:
    — 针对intermediate操作是接受一个无限大的Stream,返回一个有限的Stream;
    — 针对terminal操作,是接受一个无限大的stream,返回一个有限

    可以理解为,Intermediate操作只是注入了一个函数(比如过滤,循环等Lambda代码块)。

    比如:
    int sum = widgets.stream()
    .filter(w -> w.getColor() == RED)
    .mapToInt(w -> w.getWeight())
    .sum();


    这个代码块中:widgets.stream() 产生一个流,filter,map都是Intermediate操作,sum是一个Terminal操作;


    流的使用:
    • 数值的流有IntStream,LongStream,DoubleStream,可以直接对Int,Long,Double等原始类型进行操作。

    Intermediate操作有:

        map(maptoInt,floatMap)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unsorted
    Terminal操作有:
        forEach,forEachOrdered,toArray,reduce,collect,min,max,count,anyMatch,allMatch,noneMatch,findFirst,findAny,iterator
    Short-circuiting:
        anyMatch,allMatch,noneMatch,findFirst,findAny,limit

    典型操作:

    map/flatMap:
      把input stream的每个元素映射成为output stream的另外一个元素。
    eg.1:

    List<String> output = wordList.stream().map(String::toUpperCase).collect(Collectors.toList())


    这段代码中的map做做了一个String::toUpperCase的操作,把inputStream的每个元素进行了一个大写操作,并作为一个outputStream输出,再进行了collect操作转化为一个List。

    eg.2:

    List<Integer> nums = Arrays.asList(1,2,3,4,5);
    List<Integer) squareNums = nums.stream().map(n->n*n).collect(Collectors.toList());

    这个map操作将第一个List中的每个元素进行了平方操作。

    Map生成的映射是1:1的,每个源stream都按照规则转换为另外一个目标steram中的元素。部分情况会产生一对多的关系,1个input stream的元素可能会产生多个output stream的元素,这时候需要是用到flatMap:

    eg.3:

    Stream<List<Integer>> inputSteram = Stream.of(
       Arrays.asList(1),
       Arrays.asList(2,3,),
       Arrays.asList(4,5,6)
     );
    Stream<Integer> outputSteram = inputSteam.flatMap((childList)->childList.Stream());

    这个例子中的flatMap就将input stream中的每一个childList都进行了扁平化,将元素抽取出来放在一个output stream中,这样output stream中就没有List了,只有一堆数字。

    filter:
    filter对input stream进行测试,测试通过的元素留下来生成一个新的Stream。

    eg.4:
    Integer[] sixNums = {1,2,3,4,5,6};
    Integer[] evens = Stream.of(sixNums).filter(n->n/2==0).toArray(Integer[]::new);

    这个操作就将sixNums中的偶数挑选出来了。

    eg.5:
    List<String> output = reader.lines().
           flatMap(line->Stream.of(line.split(REGEXP)).
           filter(word->word.length()>0).
           collect(Collectors.toList()); 

    这段代码将reader中的每一行读取并进行flatMap拉平到一个新的stream,每个元素都是切割好的,并过滤出来长度大于0的单次,形成整篇文章的全部单词。

    forEach: Terminal
    forEach接收一个lambda表达式,对stream的每个元素都执行一遍表达式的内容。

    eg.6:
    workers.stream().
          filter(w->w.getGender() == Person.Sex.MALE).
          forEach(w->System.out.println(w->getName()));

    pre java8 :
    for ( Woker p : wokers) {
       if ( p.getGender() == Person.Sex.MALE){
          System.out.println(p.getName());
      }
    }

    可以明显看得出来,通过stream的操作代码变得更加简洁紧凑,更易阅读。而且当数据量较多的时候,可以使用parallelSteram.forEach来提升性能(但是要注意次序没有办法保证)。forEach 与 for循环严格意义上性能并没有太大差别,更多的在于风格差异。

    此外,forEach() 是一个terminal操作,操作完毕后stream就被消费完毕,将无法再进行使用。peek() 是一个intermediate操作,可以实现forEach()的功能,但是会返回一个新的Stream。

    eg.7.:
    Stream.of("one","two","three","four").
        filter(e->e.length()>3).
        peek(e->System.out.println("Filtered value:"+e)).
        map(String::toUpperCase).
        peek(e->System.out.println("Mapped value:" +e)).
        collect(Collectors.toList())

    forEach不能修改自己包含的本地变量只,也不能break和return。 

    findFirst: terminal & short-circuiting

    返回Stream的第一个元素,或者空。他的返回值是一个Optional,是模拟scala的一个概念,是一个容器,可能包含或者不包含。使用它可以避免NullPointerException。

    Optional可以使代码的可读性更好,同时提供了编译时检查,可以降低NPE的概率, 在编码的时候就需要处理空值问题,而不是运行时发现和debug。Stream的findAny,max/min,reduce等方法都是返回Optional值。

    reduce:terminal:
    这个方法主要作用是把Stream的元素组合起来。提供一个初始值,然后按照一定的运算规则,与前面的第一个,第二个,第n个元素组合。比如字符串拼接,数值sum,min,max,average都是特殊的reduce操作。比如sum也可以写成:

    Integer sum = integers.reduce(0,(a,b)->a+b)
    or
    Integer sum = integers.reduce(0,Integer::sum);

    如果没有提供初始值,会把Stream的前两个元素组合起来,返回一个Optional的值。

    eg.8:
    //拼接
    String concat = Stream.of("A","B","C","D","E").reduce("",Stream::concat);
    //求最小值,最大值则相反
    double minValue = Stream.of(-1.5,1.0,-3.0,-2.0).reduce(Double.MAX_VALUE,Double::min);
    //数字求和,初始为0
    int sumValue = Stream.of(1,2,3,4,5).reduce(0,Integer::sum);
    //求和,无初始值
    int sumValue = Stream.of(1,2,3,4,5).reduce(Integer::sum).get();
    //过滤再拼接
    concat = Stream.of("a","b","c","d","e","f").filter(x->x.compareTo("Z")>0).reduce("",Stream::concat));

    limit/skip:
    limit返回前n个元素,skip扔掉前n个元素;
    需要注意:
    1.短路操作,前n个元素直接跳过,执行的数量也会下降;
    2.如果与sort集成,可能会导致无法短路;
    3.并行的时候limit和skip的代价会增加

    sorted:
    对对象进行排序;
    stream的排序比数组强大,可以进行map,filter,limit,distinct让元素减少以后再进行排序,可以明显缩短执行时间。
    尽量把sort放在最后,这样代价会比较小。

    max/min/distinct:
    max和min的实现可以使用stream.sorted().findFirst,但是性能会更差。max/min的效率为O(n),sorted是O(n log n)。

    eg.9:
    List<String> words = br.lines().
    flatMap(line -> Stream.of(line.split(" "))).
    filter(word -> word.length() > 0).
    map(String::toLowerCase).
    distinct().
    sorted().
    collect(Collectors.toList());
    br.close();
    System.out.println(words);


    上述代码读入的代码进行lowerCase然后distinct排序并返回一个list;

    Match方法:
    判断元素中是否有符合条件的数据,返回一个boolean值。
    有三个match的方法,allMatch,anyMatch,noneMatch。

    自己生成Stream方法:

    Stream.of:
      — 有两种,可以接收一个变长参数,也可以接受单一值
    Stream.generate:
      — 生成一个无限长度的stream
    Stream.generate(new Supplier<Double>(){
       @override
       public Double get(){
          return Math.random();
       }
    });
    Stream.generate(()->Math.random());
    Stream.generate(Math::random);

    Stream.iterate:生成无限长度的Stream,生成的时候可以根据种子来调用指定函数生成。可以认为是seed,f(seed),f(f(seed))的循环:

    Stream.iterate(1,item->item+1).limit(10).forEach(System.out::println)



    Refer:

    印象笔记,让记忆永存。下载印象笔记

    星期二, 十一月 29, 2016

    落日

    某日,路过奥森,天色将晚,酒色的余晖洒落在公园里,有温暖的感觉。公园门口的高塔,泛出七彩,钢铁也变得温暖起来,怀念公园中奔跑的少年


    印象笔记,让记忆永存。下载印象笔记