软件开发中网站常见故障及处理方法

Posted 沛沛老爹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件开发中网站常见故障及处理方法相关的知识,希望对你有一定的参考价值。

前言

在软件开发、部署和运行中,我们经常会遇到各种个样的问题。导致系统不能按照我们的期望运行,根据工作经验,把一般常见的网站的问题总结如下。

设置问题

日志问题

  • 问题场景
    1. 所有的日志都保存在一个文件中,文件过大。例如Tomcat中不配置日志文件和路径,直接使用catalina.out进行日志的存储。会导致日志文件过大,我曾将接触过一个项目没有配置日志文件,所有的日志都在catalina.out中,根本无法下载(Windows下普通txt文件超过2G的就打不开了)。
    2. 所有的日志都存放在一个文件中,定位问题的时候,需要一行行去找。
    3. 打印日志的时候使用了错误的方式。例如System.out.println()。
  • 解决方案
    1. 应用程序的日志输出配置和第三方组建日志输出要分别配置。
      例如你使用了第三方的组件,你就为第三方的组件单独配置日志存放路径。
    2. 日志分级处理。
      检查Log配置文件,生产环境下日志输出级别应该为Warn及以上级别,检查log输出代码调用,使其符合真实日志级别有些开源的第三方组件可能不恰当的输出太多的Error日志,需要对其进行关闭。
      例如springBoot项目,一般情况下大家都会使用logback来配置logback-spring.xml文件,进行日志的分级处理。

logback-spring.xml模板 (模板来源网上,侵权请通知删除)

<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
  <!-- 定义日志的根目录 -->
  <property name="LOG_HOME" value="/app/log" />
  <!-- 定义日志文件名称 -->
  <property name="appName" value="log-springboot"></property>
 <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
  <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
    <!--
    日志输出格式:
         %d表示日期时间,
         %thread表示线程名,
         %-5level:级别从左显示5个字符宽度
         %logger50 表示logger名字最长50个字符,否则按照句点分割。 
         %msg:日志消息,
         %n是换行符
      -->
     <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%dyyyy-MM-dd HH:mm:ss.SSS [%thread] %-5level %logger50 - %msg%n</pattern>
     </layout>
 </appender>
 
 <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->  
  <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
     <!-- 指定日志文件的名称 -->
      <file>$LOG_HOME/$appName.log</file>
    <!--
     当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
     TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
     -->
     <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-   滚动时产生的文件的存放位置及文件名称 %dyyyy-MM-dd:按天进行日志滚动  
           %i:当文件大小超过maxFileSize时,按照i进行文件滚动  -->
          <fileNamePattern>$LOG_HOME/$appName-%dyyyy-MM-dd-%i.log</fileNamePattern>
          <!--   可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
           且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
             那些为了归档而创建的目录也会被删除。
                -->
     <MaxHistory>365</MaxHistory>
          <!-- 
          当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置>SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
          -->
          <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
              <maxFileSize>100MB</maxFileSize>
          </timeBasedFileNamingAndTriggeringPolicy>
      </rollingPolicy>
      <!-- 日志输出格式: -->     
      <layout class="ch.qos.logback.classic.PatternLayout">
          <pattern>%dyyyy-MM-dd HH:mm:ss.SSS [ %thread ] - [ %-5level ] [ %logger50 : %line ] - %msg%n</pattern>
      </layout>
  </appender>

  <!-- 
      logger主要用于存放日志对象,也可以定义日志类型、级别
      name:表示匹配的logger类型前缀,也就是包的前半部分
      level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
      additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
      false:表示只用当前logger的appender-ref,true:
      表示当前logger的appender-ref和rootLogger的appender-ref都有效
  -->
  <!-- hibernate logger -->
  <logger name="com.atguigu" level="debug" />
  <!-- Spring framework logger -->
  <logger name="org.springframework" level="debug" additivity="false">   
   <!-- 给某个包或类指定logger和appender -->    
  <appender-ref ref="xxxxx" />  </logger>

<!-- 
  root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
  要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 
  -->
  <root level="info">
      <appender-ref ref="stdout" />
      <appender-ref ref="appLogAppender" />
  </root>
  <!--springboot中多环境配置文件-->
  <springProfile name="prod">
      <root level="info">
          <!-- 生产环境最好不配置console写文件 -->
         <appender-ref ref="DEBUG_FILE" />
         <appender-ref ref="INFO_FILE" />
          <appender-ref ref="WARN_FILE" />
          <appender-ref ref="ERROR_FILE" />
          <appender-ref ref="ALL_FILE" />
      </root>
      <logger name="com.xusanduo.demo" level="warn"/> <!-- 生产环境, 指定某包日志为warn级 -->
      <logger name="com.xusanduo.demo.MyApplication" level="info"/> <!-- 特定某个类打印info日志, 比如application启动成功后的提示语 -->
  </springProfile>
</configuration>
  • 小结
    请大家记住,应用使用日志的目的,主要是为了我们能够快速的定位问题,帮助我们快速的发现和定位问题。我们应该在项目上线后,定期检查日志情况(包括但不限于日志内容、日志大小、日志格式是否规范等)。

日志分级说明
logback定义了8个级别的log(除去OFF和ALL,可以说分为6个级别),优先级从高到低依次为:OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL
级别说明

  1. OFF 最高等级的,用于关闭所有日志记录。

  2. FATAL 指出每个严重的错误事件将会导致应用程序的退出。这个级别比较高了。重大错误,这种级别你可以直接停止程序了。

  3. ERROR 指出虽然发生错误事件,但仍然不影响系统的继续运行。打印错误和异常信息,如果不想输出太多的日志,可以使用这个级别。

  4. WARN 表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示。

  5. INFO 消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印过多的日志。曾经就有项目死在了日志打印太多导致访问变慢的情况。

  6. DEBUG 指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息。

  7. TRACE 很低的日志级别,一般不会使用。

  8. ALL 最低等级的,用于打开所有日志记录。

缓存故障问题

  • 问题场景
    1. 缓存导致内容回显错误。
    2. 缓存导致页面访问错误。
  • 解决方案
    1. 当缓存已经不仅仅是改善性能,而是成为网站不可或缺的一部分的时候,对缓存的管理级别要提高。
  • 小结
    1. 缓存的使用场景
    2. 对于和持久层交互的时候,是先缓存还是先持久化这个需要根据业务场景来考量。

应用启动不同步问题

  • 问题场景
    1. 应用启动成功但是服务报错,例如一直调用指定接口失败(微服务中会更加直观,对应的服务接口找不到)。
    2. 应用无法启动成功,直接回滚。
  • 解决方案
    1. 在应用程序启动前,一定要先检查其依赖项已经完全启动了。
  • 小结
    1. 在服务划分的时候,一定要注意不要出现循环依赖的问题。虽然SpringBoot给我们提供了一个便捷的解决方式,但是Spring也给我们友好的提示了,最好不要有循环依赖。在这里,一般就涉及到了服务和接口的划分原则。
    2. 依赖项要先启动,否则的话,当前服务也无法提供完整的服务(特殊情况除外,例如电商平台的大促等活动情况中,有些服务对于业务来讲,优先级不是那么高的,我们可以手动将部分服务kill掉,但是这种情况,不在我们当前的讨论范围之类)

    扩展 类设计五大原则(SOLID设计原则)
    说明:JAVA中类设计原则,同样适用于微服务接口模块等
    SRP(Single Responsibility Principle)单一职责原则:所有的类应该不要包含非本类的功能属性。例如设计鸡类的时候,就不要把鸭子的属性放到里面去,否则就变成了鸡同鸭讲了。
    最高端的食材,往往只需要最简单的烹饪。记住这句话,你就知道类的设计要简单来设计了。
    OCP(Open Closed Principle) 开放关闭原则:对扩展开放,对修改关闭。类独立之后就不应去修改它,而是以扩展的方式适应新需求。例如有个鸡类,如果有个肯德鸡,这个时候不应该在鸡类里面去修改,而是创建一个新的肯德鸡类。
    LSP(Liskov Substitution Principle)里氏替换原则:继承。派生类可以替换基类来使用。
    ISP(Interface Segregation Principle)接口隔离原则:接口应该拥有单一的功能行为。
    DIP(Dependence Inversion Principle)依赖倒置原则:高层模块不应该依赖于低层模块,二者之间都应该依赖于其各自的抽象;抽象不应该依赖于细节,而细节应该依赖于抽象。

习惯问题

滥用生产环境

  • 问题场景
    1.测试场景中直接调用生产数据。例如有的团队支付线上和测试使用的同一个账号,就直接调用了生产的支付,导致第三方支付回调处理产生异常数据。
    2.使用Navi cat连接mysql的时候,测试和生产都连接上(navicat支持多数据源多窗口模式),然后在处理数据的时候,随意切换窗口,不小心就操作到了生产环境数据。刚出来实习的时候就看到一个技术经理一个疏忽,删掉了订单表50W条数据。
  • 解决方案
    1.访问正式环境要规范,不小心就会出问题。
    2.正式环境不要给太多人多权限,按照流程规范走。
  • 小结
    1.尽量创造条件,让生产和非生产数据拆分,不混用。
    2.正式环境的操作要按照规范流程走,宁愿慢一点也不要错。

不规范的流程问题

  • 问题场景
    1.代码提交无序混乱,代码提交冲突。这个在团队开发中经常能遇到的问题。
    2.代码拆分支的时候随意,导致后续合并代码混乱。
    3.合并的代码没有经过review直接合并,导致功能异常。
  • 解决方案
    1.代码提交前一定要用diff命令进行代码比较,确认没有提交不该提交的代码。
    2.加强Code review,代码在正式提交前必须被至少一个其他工程师做过Code review。
  • 小结
    涉及到git的管理的时候,最好先定义好对应的规范流程。是走集中式工作流 、功能分支工作流 、Gitflow工作、 还是Forking工作流方式,这个要在使用之前就定义好。这样在创建分支的时候也不会乱。

    扩展:git工作流常见方式
    1、集中式工作流

    特点:只有一个master分支,修改和提交都在本地操作,开发者会先把远程的仓库克隆到本地,直到在某个合适的时间点将本地的代码合入到远程master。

    2、功能分支工作流

    特点:不直接往master提交代码,而是从master拉 取feature分支进行功能开发,团队成员根据分工拉取不同的功能分支来进行不同的功能开发,这样 就可以完全隔离开每个人的工作。当功能开发完成后,会向master分支发起Pull Request,只有审核通过的代码才真正允许合入master,这样就加强了团队成员之间的代码交流。

    3、Gitflow工作流

    特点:非常适合用来管理大型项目的发布和维护,master和develop分支一直存在,master分支可以被视为稳定的分支, 而develop分支是相对稳定的分支,特性开发会在feature分支上进行,发布会在release分支上 进行,而bug修复则会在hotfix分支上进行。

    4、Forking工作流

    特点:github开源项目。它有一个公开的中央仓库,其他贡献 者可以Fork这个仓库作为你自己的私有仓库,项目维护者可以直接往中央仓库 push代码,代码贡献者只能将代码push到自己的私有仓库,只有项目维护者接受代码贡献 者往中央仓库发起的pull request才会真正合入。

不好的编程习惯

  • 问题场景
    1. 空对象异常导致OOM。作为引用对象,对象不存在(或未初始化)导致此问题。
    2. 对象比较异常。例如java代码中对象的比较使用==。
    3. 其它未捕获的异常,导致程序报错的问题。
    4. 代码中命名不规范的问题,如果业务变更多次,同时换了几波人。剩下的就是一个个粪坑了。
  • 解决方案
    1. 程序在处理一个输入对象时,如果不能明确该对象是否为空,必须做空对象判断。例如Java中,一般会要求在编码的时候,先使用if判断当前对象是否为空。
    2. 程序在调用其他方法时,输入的对象尽量保证不是null,必要时构造空对象(使用空对象模式)。
  • 小结
    1.研发流程规范在编码之前一定要定义好,并且和团队宣讲好,磨刀不误砍柴工。如果实在来不及,也可以使用开源的第三方的规范,例如阿里的或者谷歌的。

    在IDEA中可以直接使用 Alibaba Java Coding Guidelines插件来规范写代码,它可以智能的检测出代码的常规错误。
    使用Sonar进行扫码,基本上可以规避90%的常见bug和性能问题。

编码问题

大文件读写独占磁盘的问题

  • 问题场景
    1.上传视频的时候导致整个网站访问非常慢。
  • 解决方案
    1.将图片视频和当前网站拆分到不同的网站,以减轻网站的负载。
  • 小结
    使用OSS等对象存储功能,将网站的图片、视频和一些可以拆分的功能放到不同的网站中去,这样即使某个功能异常,不会影响整体网站的功能和用户的整体体验。

高并发访问数据库

  • 问题场景
    网站高频页面直接访问了数据库,导致网站整体性能下降。
  • 解决方案
    1.首页不应该访问数据库,首页需要的数据可以从缓存服务器或者搜索引擎服务器获取。
    2.首页最好是静态。这样在CDN的时候可以更快。
  • 小结
    1.高频访问最好不要直连,中间最好使用缓存等中间件。帮助网站可以更好的解耦的同时,可以更好的提升性能。

高并发下的锁问题

  • 问题场景
  1. 并发访问下超时场景。某单例对象中多处使用了synchronized,其中某个需要远程调用的操作也用了synchronized,这个操作只是偶尔会被执行,但是每次执行都需要较长的时间才能完成。
  • 解决方案
    谨慎的使用锁
  • 小结
    在涉及到接口的时候,请谨慎的使用锁。

    synchronized 在 JDK 1.6 中有大量优化:分级锁(偏向锁、轻量级锁、重量级锁)、锁消除、锁粗化等。

总结

网站中的故障有部分是客观问题,需要对应的编程老师傅(踩过坑)的带领下,根据实际情况来估算;还有一部分是因为团队习惯等问题,这个需要团队制定良好的规范和流程来避免(编程不规范,新人两行泪)。
作为一名合格的技术管理者或者技术专家,应该在团队中定义好对应的技术研发规范和流程,以来预防一些常见的问题。

以上是关于软件开发中网站常见故障及处理方法的主要内容,如果未能解决你的问题,请参考以下文章

电脑故障排除

Linux常见运维故障及处理方法分享(持续更新)

常见服务器故障类型及排查方法总结

常见的宽带连接错误的处理办法

一个电脑故障,请高人解决

九阳豆浆机常见故障及处理办法