01 background

Let each user get a stable and timely page experience, is the direction of front-end engineers have been working hard.

As a video website with rich content resources, the main site of iQiyi’s official website needs to make frequent adjustments to the online or offline programs and the configuration of various activities. It has high requirements for the availability and stability of the SSR service on the page.

Before 2019, the SSR of the main website page of iQiyi official website adopts Velocity template written in CMS platform and compiled by Java. The advantage is fast rendering speed, but the disadvantages are also very obvious:

(1) Poor development experience in CMS platform: it is not as convenient as traditional IDE, shortcut keys cannot be configured, plug-ins cannot be installed, etc., resulting in low development efficiency.

(2) Different codes at the front and back ends: The back end uses Velocity template while the front end needs to use Vue, resulting in different codes at the front and back ends.

(3) Destruction of encapsulation of Vue components: Since Java cannot compile Vue components, all Vue components need to be written in the CMS platform in the form of Slot to achieve the purpose of SEO and SSR.

For all the above reasons, we decided to use Node for SSR. Because our front-end framework is Vue, we chose the supporting Nuxt framework for SSR.

The difficulty of using Nuxt for SSR is not how to use Nuxt, but how to maintain the service and ensure its performance and stability. Therefore, this article will not introduce the use of Nuxt, and its syntax can be refer to the official website. This article mainly introduces how to ensure the availability and stability of Nuxt service from the aspects of performance, cache, traffic limiting, disaster recovery, and logging.

02 Nuxt stability improvement road

2.1 Page Configuration

I’ll start with an important configuration file. In the root directory of our project, we created a page configuration file to store the general configuration for each page, such as the page cache configuration, Purge information, theme color configuration, advertising information configuration, etc. This file exports an Object with the key Value of the page Router Name and Value of the page configuration information:

// configs/pageinfo.js export default { 'dianshiju-id': {... }, 'zongyi: {theme:' dark ', / / the colour theme page configuration}, 'home2020: {... }, 'rank-hot': {... }}Copy the code

Then we read the corresponding page configuration in Nuxt plug-in according to the requested routing information and inject it into all component instances for easy access:

// plugins/pageinfo.js import config from 'configs/pageinfo.js' export default ({ route }, Inject) => {inject('pageInfo', config[route.name]) // Inject page configuration information}Copy the code

So you can get the page configuration information from anywhere in the component without having to pass it through layers of Props, and the common page configuration is not scattered around the project for unified management.

<div :class="$pageinfo.theme "> </div>Copy the code

2.2 Browser Compatibility

Although Nuxt can theoretically support IE9, there are many aspects of IE9 that require Polyfill, such as support for the History API. To keep the code simple, we dropped support for IE9-, but we still have a mechanism in the framework to support jQuery. This allows high and low versions to share HTML without having to write separate templates for lower versions, minimizing the cost of compatibility with lower versions of browsers.

Nuxt provides a ‘render. Route’ hook function that executes after the HTML is generated and before it is returned to the user. In this hook function, we can determine the user version ** based on the UA information requested by the user. If the user is a lower-version browser user, we can remove the higher-version JS from the HTML and inject the lower-version packaged entry file.

// nuxt.config.js 'render:route': (url, result, {req}) => {if (isLowBrowser(req)) {const $= cheerio.load(result.html) $('body ') const $= cheerio.load(result.html) $('body ' Script [SRC * = \ 'PCW/SSR \'] '). The remove () / / removing high version js $(" body "). Append (' < script SRC = "/ / stc.iqiyipic.com/jquery.js" > < / script ') Add jquery / / $(" body "). Append (' < script SRC = "/ / stc.iqiyipic.com/index.js" > < / script ') / / add low version entrance js result. The HTML = $. The HTML ()} },Copy the code

2.3 Performance Optimization

2.3.1 Data Filtering

An important mechanism of Nuxt is that it will hang all the data returned by asyncData function on window.__nuxt__ and return it to the client via HTML, so that the client will not request the data again. Therefore, the amount of data returned by asyncData function becomes more important for performance. This affects not only the transfer time of the interface data, but also the size of the HTML. Therefore, we need to compress this data. In NUXT, we tried three solutions:

  1. Do data filtering in asyncData

  2. GraphQL

  3. Data filtering platform

Data filtering in asyncData only reduces THE size of HTML, but does not reduce the transmission of redundant data.

GraqhQL solves the problem of transferring redundant data, but the code is not maintainable because it requires writing a lot of query parameters and using POST when query parameters are too long.

// Very unmaintainable query string
const query = ` 
query {
  qipuGetVideoBriefList (
    album_id: "${params.album_id}" type: "EPISODE_LIST" play_platform: "PC_QIYI" order: "DESC" ) { rpc_status episode { id g_corner_mark_s brief { title short_title subtitle page_url } release { publish_time }}}} '
axios.get(`http://xxx.iqiyi.com/graphql?query=${query}`)
Copy the code

Finally, we build a data filtering platform, configure interface data sources, field filtering and mapping of data in a visual way, and finally generate an interface, which obtains data from the configured data sources, and then returns only the fields we need through field mapping and field filtering. This filters out redundant data without maintaining GraphQL query parameters and visualizes GraphQL query strings as configurations.

2.3.2 Layout

Nuxt provides Layout configuration items, which seem very convenient, but by analyzing the.nuxt/ app.js entry file generated by Nuxt, we found that all layouts are packaged, whether they are used or not. For example, if A page uses LayoutA, B page uses LayoutB, and C page uses LayoutC, the entry JS of A, B, and C pages will have all the codes of LayoutA, LayoutB, and LayoutC.

// .nuxt/App.js import _8daa19aa from '.. /src/layouts/a.vue' import _8daa19a8 from '.. /src/layouts/b.vue' import _8daa19a6 from '.. /src/layouts/c.vue' import _6f6c098b from './layouts/default.vue'Copy the code

Therefore, if the Layout logic is complex, and if the code is large, the JS size of all pages will be much larger. For the above reasons, we abandoned Layout using Nuxt and instead packaged an I71Layout component to provide common functionality across all pages to reduce redundant code. Since Vue SSR is based on virtual DOM, while Java is based on string, the performance is slower than before, so we make caching strategy from two granularity of page and component.

2.4 the cache

We use the Nginx reverse proxy to control page-level caching, which defaults to 5 minutes per page. When Nuxt returns non-200, Nginx uses an expired cache.

Component caching We use the official @nuxtjs/component-cache module, which provides a serverCacheKey configuration item whose value Nuxt uses as the cache Key. Therefore, we defined a cache-key Props for each component that needs to be cached. If the value is passed, the Props will be cached based on the passed value. If the value is not passed, the Props will not be cached. In this way, for all uncached pages, a cache-key can be passed when the component is called to make the component cached, thus speeding up the SSR of the page.

2.5 purge

For cached pages, we need the corresponding Purge interface to clear the page cache. The page is divided into two parts, one is our Nginx reverse proxy cache Purge, the other is the CDN cache Purge, they are the same principle, so we will only talk about the Nuxt service Nginx reverse proxy cache Purge.

We want to provide a Purge interface to Purge the specified page by passing the page name argument. Our Nuxt framework itself is built on Koa, so all we need to do is plug in the KOA-Router before SSR to provide our Purge interface.

// server/index.js
const app = new Koa()
const router = new Router()
router.get('/api/purge/page/:pageName'.async (ctx) => { // Define the Purge interface, which supports passing pageName
  ctx.body = await purgePage(ctx) // Purge nginx cache and CDN cache
})
app.use(router.routes()) // Insert the API we need
app.use(ctx= > { // NuxT for SSR
    nuxt.render(ctx.req, ctx.res)
})
Copy the code

So how do we know which urls to Purge for each pageName? Here we need to do something in the page configuration file mentioned earlier to associate the pageName and Purge urls:

// configs/pageinfo.js
zongyi: {
   purge: {
      purgeUrl: [
        'https://zongyi.iqiyi.com/'.'https://www.iqiyi.com/zongyi',}}Copy the code

Then we just need to Purge the urls from all the services deployed on the company’s app platform, with four clusters and hundreds of Docker containers, and Purge the Nginx cache from all the hosts as follows.

First we need to configure Nginx to support Purge:

location / {
    proxy_cache_purge PURGE from all;
}
Copy the code

To purge the cache corresponding to the host uri, call http://{host domain name}:{host port}/purge/{uri}.

To Purge the page cache on all hosts, simply call the Purge interface on each host one at a time.

2.6 current limiting

For pages without cache, traffic limiting is required to prevent malicious flushing behavior. We have implemented three restrictions in terms of WAF, single-IP traffic limiting and IP blacklist.

2.6.1 WAF (Web Application Firewall)

First we access the company’s firewall platform, through intelligent recognition to filter out some malicious requests. Secondly, for some dynamically routed pages, we conduct a re match for the requested URL, and all the requests that do not meet the re are denied access and return 403.

2.6.2 Single-IP Traffic Limiting

To prevent single-IP script deluge, we use the limit_req module for single-IP traffic limiting on the Nginx reverse proxy. For common users and crawlers, we set different access frequencies. Requests exceeding the frequency are denied access and 503 is returned.

2.6.3 IP Address Blacklist

In addition, through log analysis, we can find some obvious brush IP, for such IP, we want to ban directly.

If you add a Deny statement directly to the Nginx configuration, you will find that the Deny does not take effect because the request passes through the gateway. When the request reaches our Nginx service, the Remote Address becomes the gateway IP Address, and we Deny the real user IP Address. So we need to find a way to let Nginx know what the user’s real IP is.

Each user’s real IP address is stored in the X-Forwarded-For field. To retrieve the user’s real IP address, configure the following configuration in Nginx:

# nginx.conf

server {
    real_ip_header X-Forwarded-For; Http_forwarded-for tells Nginx that the user's real IP address is stored in the X-Forwarded-For field
    real_ip_recursive on;
}
Copy the code

This configuration is not enough. The x-Forwarded-For field is a string. Each time this field passes through a node, it adds an IP address to it. {user’s real IP address},{gateway IP address}, and Nginx reads the IP address from back to front by default. If the IP address is a trusted IP address, Nginx reads the IP address from back to front until the untrusted IP address is regarded as the real IP address of the user. Therefore, without additional configuration, Nginx reads the IP address of the gateway. Therefore, we also need to add all gateway IP addresses to the list of trusted IP addresses before Nginx can proceed to read the user’s real IP address. We can set the entire Intranet segment as a trusted IP:

# nginx.conf

server {
	set_real_ip_from xxx.0.0.0/8; Set the internal network segment to a trusted IP address
    real_ip_header X-Forwarded-For; Http_forwarded-for tells Nginx that the user's real IP address is stored in the X-Forwarded-For field
    real_ip_recursive on;
}
Copy the code

Now that Nginx can read the user’s real IP address, all we need to do is create an IP blacklist:

# nginx.conf server {set_real_ip_from xxx.0.0.0/8; Http_forwarded-for real_IP_header X-Forwarded-for Http_forwarded-for (http_forwarded-for) {http_forwarded-for (http_forwarded-for) {http_forwarded-for (http_forwarded-for); Include ip-blacklist.conf # include ip-blacklist.confCopy the code
# ip-blacklist.conf

deny xx.xx.xx.xx;

Copy the code

2.7 disaster preparedness

For uncached pages, in addition to limiting the flow, we need to haveOtherwise, if the service fails, the user will see the error page.

We deployed a set of independent disaster recovery services, using Node scripts to pull all important pages from online services every three minutes, if the page returns 200, it will be stored as HTML files, otherwise discard the page, and then use Nginx as a reverse proxy to Serve the disaster recovery pages.

The CDN first pulls the page from the online service. If the value is not 200, the CDN pulls the corresponding page from the Dr Service and returns it to the user. In this way, the user will never see an error page.

2.8 Server Logs

Server logs are mainly used to record Nuxt render page records, error messages, etc., they are very important for troubleshooting, traffic statistics, our server logs are divided into two parts: page render logs, interface request logs.

Page rendering log refers to writing a log every time a page request is made, recording the URL, Referer, user Cookie, user IP and other information of the page. If there is no error in page rendering, it will be written into logs/page/info.log. Write a log to logs/page/error.log.

The interface log is the request log issued during each page rendering, encapsulated in the HTTP function that sends the request at the bottom level, and records the page URL, interface URL, interface parameters and other information. If the request is successful, a log is written to logs/ API /info.log. Write a log to logs/ API /error.log.

// nuxt.config.js

hooks: {
   'render:setupMiddleware': app= > { // A middleware is inserted at nuxT initialization and a logParams object is generated for each request
      app.use(async (req, res, next) => {
        req.logParams = { 
          requestId: generateRandomString(), // Generate a random requestId string
          pageUrl: req.url
        }
        next()
      })
    },
    'render:routeDone': (url, result, { req, res }) = > { // Render complete
      logger.page.info({ type: 'render'. req.logParams}, req)// Add requestId when writing logs
    },
    'render:errorMiddleware': app= > app.use(async (error, req, res, next) => { // Render error
      logger.page.error({ type: 'render', error, ... req.logParams }, req)// Error logs are loaded with requestId

      next(error)
    }),
}
Copy the code

In order to connect the page rendering log with the interface log of this rendering, we will generate a unique RequestId before rendering, and then carry this RequestId in all the logs of this rendering. Then we can query the page rendering log through a RequestId. And all requests made to this page.

class Resource {
  async http (opts)
    let data
    try {
        data = await axios(opts)
        process.server && logger.api.info(opts, this.req.logParams) // Add requestid to the API log
	} catch (error) {
	    process.server && logger.api.error(opts, error, this.req.logParams) // Add requestid to the API error log
	}
	return data
  }
}
Copy the code

2.9 Log Collection

We use Filebeat + Elasticsearch + Kibana for log management. Firstly, we collect real-time logs through Filebeat, and then report them to the specified Kafka cluster. Then we analyze and index the logs, and finally generate a visual log query page. This allows us to view logs that match the query criteria over a period of time.

2.10 Traffic Monitoring

Based on the server logs, we can count the traffic through CDN cache, WAF interception, Nginx reverse proxy cache, and finally calculate the actual traffic to our Nuxt service. We can filter out logs with type= ‘render’ in the specified time period according to the time field, which is the total traffic borne by the Nuxt service in this time period. If you want to see the traffic of each page, you can further filter the pageUrl field in the log.

03 summary

Nuxt fundamentally solves all the problems encountered in CMS development using Velocity, but it also brings some other problems, such as domain name conflicts, server-side variable sharing, rendering performance problems, etc. But in general, the defects do not overshadow the defects. The development experience has been improved qualitatively, and the development efficiency has been increased by more than 50%. Component reuse rate is higher, component encapsulation is better, code readability and maintainability have been greatly improved; Under the strong support of CDN cache, Nginx reverse proxy cache and component cache, the page rendering performance does not decline. Due to the removal of some complex logic, such as inconsistent front-end and back-end codes and the extensive use of Slot, the rendering performance of the first screen is greatly improved. The server response time is about 0.5s on average, the error rate is about 0.2%, and the accessibility is almost 100% in the case of the disaster recovery service.

Finally, look forward to the arrival of Nuxt3 and further improvements in performance and development experience.