Re: [PATCH/RFC] NFSv3 handle out-of-order write replies

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Thu, 26 Jan 2023, Jeff Layton wrote:
> On Wed, 2023-01-25 at 13:03 +1100, NeilBrown wrote:
> > NFSv3 includes pre/post wcc attributes which allow the client to
> > determine if all changes to the file have been made by itself, or any
> > might have been made by some other client.  IF there are gaps in the
> > pre/post ctime sequence it must be assumed that some other client caused
> > that change in that gap and the local cache must be suspect.  The next
> > time the file is opened, the cache should be invalidated.
> > 
> > Since Commit 1c341b777501 ("NFS: Add deferred cache invalidation for
> > close-to-open consistency violations") (I think) in linux 5.3 the Linux
> > client has been triggering this invalidation.  The chunk in
> > nfs_update_inode() in particularly triggers.
> > 
> > Unfortunately Linux NFS assumes that all replies will be processed in
> > the order sent, and will arrive in the order processed.  This is not
> > true in general.  Linux might ignore the wcc info in a WRITE reply -
> > even though the pre-ctime might match the current iversion - because the
> > reply is for a request that was sent before some other request for which
> > a reply has already been seen.  This is detected by Linux using the
> > gencount tests in nfs_inode_attr_cmp().
> > 
> > Also, when the gencount tests pass it is still possible that the
> > requests were processed on the service in a different order, and a gap
> > in the ctime sequence might be filled in by a subsequent reply.  Linux
> > NFS does not try to detect this.
> > 
> > The net result is that writing to a server and then reading the file
> > back can result in going to the server for the read rather than serving
> > it from cache - all because a couple of replies arrived out-of-order.
> > This is a performance regression over kernels before 5.3 (though that
> > change is a correctness improvement).
> > 
> > This has been seen with Linux writing to a Netapp server which can
> > occasionally re-order requests.  I have also demonstrated it with a
> > modified Linux server which adds pre/post attributes to V3 write
> > replies, and occasionally adds a delay to  force reply reordering.
> > 
> > This patch attempts to address the problem by replacing the
> > NFS_INO_DATA_INVAL_DEFER flag with a small array of segments in the
> > ctime sequence which have not yet been seen in replies.  Rather than
> > setting the flag, we add the missing segment to the array.  Rather
> > than testing the flag, we check if the array is empty.
> > 
> > The array contains A,B pairs when the local iversion is advanced from A
> > to B, and B,A pairs when a reply tells us that the ctime (aka the
> > change-id) has advanced from A to B on the server.
> > Overlapping segments are merged and inverted segments cancel (which
> > effectively comes to the same thing).  Empty segments are removed.
> > 
> > An expected reply (probably the common case) will have the server say
> > that ctime went from A to B, and Linux will update the iversion from A
> > to B.  Rather than adding A,B and the B,A which will cancel out, we
> > simply don't bother.
> > 
> 
> So your cache remains intact as long as you're not checking validity
> before all of the replies come in.

Exactly - and we primarily check validity at 'open' (or lock).

> 
> Clever! I like this idea. 

Thanks :-)

> 
> > One difficulty in this is that in some circumstances,
> > NFS_ATTR_FATTR_PRECHANGE is cleared.  This causes the gap tracking to
> > fail because we can only add pre/post attributes to the table if bother
> > NFS_ATTR_FATTR_PRECHANGE and NFS_ATTR_FATTR_CHANGE are set.  If one is
> > cleared, we have a problem.
> > 
> > This patch works around that by introducing a new
> > NFS_ATTR_FATTR_PRECHANGE_RAW whcih is set when the normal PRECHANGE is
> > cleared.  If either PRECHANGE or PRECHANGE_RAW a set then we can use the
> > prechange value.  Obviously this is a hack.  I would be keen to
> > understand why PRECHANGE is being cleared so I can find a better
> > resolution.
> > 
> > I haven't given much thought to directories.  A number of places call
> > 	nfs_set_cache_invalid(inode, NFS_INO_INVALID_DATA);
> > 
> > only on directories and I don't understand why.  As directories still
> > have pre/post attributes even in NFSv4 it would be good to understand
> > how to get this right.
> > 
> > Any help would be most appreciated.
> > 
> > Thanks,
> > NeilBrown
> > 
> > 
> > diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c
> > index f594dac436a7..8535372861e9 100644
> > --- a/fs/nfs/dir.c
> > +++ b/fs/nfs/dir.c
> > @@ -84,7 +84,7 @@ alloc_nfs_open_dir_context(struct inode *dir)
> >  		ctx->dtsize = NFS_INIT_DTSIZE;
> >  		spin_lock(&dir->i_lock);
> >  		if (list_empty(&nfsi->open_files) &&
> > -		    (nfsi->cache_validity & NFS_INO_DATA_INVAL_DEFER))
> > +		    nfs_ooo_test(nfsi))
> >  			nfs_set_cache_invalid(dir,
> >  					      NFS_INO_INVALID_DATA |
> >  						      NFS_INO_REVAL_FORCED);
> > diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c
> > index 6b2cfa59a1a2..4c9ad2e6586e 100644
> > --- a/fs/nfs/inode.c
> > +++ b/fs/nfs/inode.c
> > @@ -208,11 +208,12 @@ void nfs_set_cache_invalid(struct inode *inode, unsigned long flags)
> >  
> >  	nfsi->cache_validity |= flags;
> >  
> > -	if (inode->i_mapping->nrpages == 0)
> > -		nfsi->cache_validity &= ~(NFS_INO_INVALID_DATA |
> > -					  NFS_INO_DATA_INVAL_DEFER);
> > -	else if (nfsi->cache_validity & NFS_INO_INVALID_DATA)
> > -		nfsi->cache_validity &= ~NFS_INO_DATA_INVAL_DEFER;
> > +	if (inode->i_mapping->nrpages == 0) {
> > +		nfsi->cache_validity &= ~NFS_INO_INVALID_DATA;
> > +		nfs_ooo_clear(nfsi);
> > +	} else if (nfsi->cache_validity & NFS_INO_INVALID_DATA) {
> > +		nfs_ooo_clear(nfsi);
> > +	}
> >  	trace_nfs_set_cache_invalid(inode, 0);
> >  }
> >  EXPORT_SYMBOL_GPL(nfs_set_cache_invalid);
> > @@ -677,9 +678,10 @@ static int nfs_vmtruncate(struct inode * inode, loff_t offset)
> >  	trace_nfs_size_truncate(inode, offset);
> >  	i_size_write(inode, offset);
> >  	/* Optimisation */
> > -	if (offset == 0)
> > -		NFS_I(inode)->cache_validity &= ~(NFS_INO_INVALID_DATA |
> > -				NFS_INO_DATA_INVAL_DEFER);
> > +	if (offset == 0) {
> > +		NFS_I(inode)->cache_validity &= ~NFS_INO_INVALID_DATA;
> > +		nfs_ooo_clear(NFS_I(inode));
> > +	}
> >  	NFS_I(inode)->cache_validity &= ~NFS_INO_INVALID_SIZE;
> >  
> >  	spin_unlock(&inode->i_lock);
> > @@ -1101,7 +1103,7 @@ void nfs_inode_attach_open_context(struct nfs_open_context *ctx)
> >  
> >  	spin_lock(&inode->i_lock);
> >  	if (list_empty(&nfsi->open_files) &&
> > -	    (nfsi->cache_validity & NFS_INO_DATA_INVAL_DEFER))
> > +	    nfs_ooo_test(nfsi))
> >  		nfs_set_cache_invalid(inode, NFS_INO_INVALID_DATA |
> >  						     NFS_INO_REVAL_FORCED);
> >  	list_add_tail_rcu(&ctx->list, &nfsi->open_files);
> > @@ -1344,8 +1346,8 @@ int nfs_clear_invalid_mapping(struct address_space *mapping)
> >  
> >  	set_bit(NFS_INO_INVALIDATING, bitlock);
> >  	smp_wmb();
> > -	nfsi->cache_validity &=
> > -		~(NFS_INO_INVALID_DATA | NFS_INO_DATA_INVAL_DEFER);
> > +	nfsi->cache_validity &= ~NFS_INO_INVALID_DATA;
> > +	nfs_ooo_clear(nfsi);
> >  	spin_unlock(&inode->i_lock);
> >  	trace_nfs_invalidate_mapping_enter(inode);
> >  	ret = nfs_invalidate_mapping(inode, mapping);
> > @@ -1807,6 +1809,38 @@ static int nfs_inode_finish_partial_attr_update(const struct nfs_fattr *fattr,
> >  	return 0;
> >  }
> >  
> > +static void nfs_ooo_merge(struct nfs_inode *nfsi,
> > +			  u64 start, u64 end)
> > +{
> > +	int i, cnt;
> > +
> > +	/* add this range, merging if possible */
> > +	cnt = nfsi->ooo_cnt;
> > +	for (i = 0; i < cnt; i++) {
> > +		if (end == nfsi->ooo[i][0])
> > +			end = nfsi->ooo[i][1];
> > +		else if (start == nfsi->ooo[i][1])
> > +			start = nfsi->ooo[i][0];
> > +		else
> > +			continue;
> > +		/* Remove 'i' from table and loop to insert the new range */
> > +		cnt -= 1;
> > +		nfsi->ooo[i][0] = nfsi->ooo[cnt][0];
> > +		nfsi->ooo[i][1] = nfsi->ooo[cnt][1];
> > +		i = -1;
> > +	}
> > +	if (start != end && cnt < ARRAY_SIZE(nfsi->ooo)) {
> > +		nfsi->ooo[cnt][0] = start;
> > +		nfsi->ooo[cnt][1] = end;
> > +		cnt += 1;
> > +	}
> > +	if (cnt > nfsi->ooo_max) {
> > +		printk("ooo_max -> %d\n", cnt);
> > +		nfsi->ooo_max = cnt;
> > +	}
> > +	nfsi->ooo_cnt = cnt;
> > +}
> > +
> >  static int nfs_refresh_inode_locked(struct inode *inode,
> >  				    struct nfs_fattr *fattr)
> >  {
> > @@ -1817,8 +1851,17 @@ static int nfs_refresh_inode_locked(struct inode *inode,
> >  
> >  	if (attr_cmp > 0 || nfs_inode_finish_partial_attr_update(fattr, inode))
> >  		ret = nfs_update_inode(inode, fattr);
> > -	else if (attr_cmp == 0)
> > -		ret = nfs_check_inode_attributes(inode, fattr);
> > +	else {
> > +		if ((fattr->valid & NFS_ATTR_FATTR_CHANGE) &&
> > +		    (fattr->valid & (NFS_ATTR_FATTR_PRECHANGE |
> > +				     NFS_ATTR_FATTR_PRECHANGE_RAW)))
> > +			nfs_ooo_merge(NFS_I(inode),
> > +				      fattr->change_attr,
> > +				      fattr->pre_change_attr);
> > +
> > +		if (attr_cmp == 0)
> > +			ret = nfs_check_inode_attributes(inode, fattr);
> > +	}
> >  
> >  	trace_nfs_refresh_inode_exit(inode, ret);
> >  	return ret;
> > @@ -1909,6 +1952,8 @@ int nfs_post_op_update_inode_force_wcc_locked(struct inode *inode, struct nfs_fa
> >  	if (attr_cmp < 0)
> >  		return 0;
> >  	if ((fattr->valid & NFS_ATTR_FATTR) == 0 || !attr_cmp) {
> > +		if (fattr->valid & NFS_ATTR_FATTR_PRECHANGE)
> > +			fattr->valid |= NFS_ATTR_FATTR_PRECHANGE_RAW;
> >  		fattr->valid &= ~(NFS_ATTR_FATTR_PRECHANGE
> >  				| NFS_ATTR_FATTR_PRESIZE
> >  				| NFS_ATTR_FATTR_PREMTIME
> > @@ -2084,8 +2129,13 @@ static int nfs_update_inode(struct inode *inode, struct nfs_fattr *fattr)
> >  				dprintk("NFS: change_attr change on server for file %s/%ld\n",
> >  						inode->i_sb->s_id,
> >  						inode->i_ino);
> > -			} else if (!have_delegation)
> > -				nfsi->cache_validity |= NFS_INO_DATA_INVAL_DEFER;
> > +			} else if (!have_delegation) {
> > +				if (fattr->valid & NFS_ATTR_FATTR_PRECHANGE)
> > +					nfs_ooo_merge(nfsi, fattr->change_attr,
> > +						      fattr->pre_change_attr);
> > +				nfs_ooo_merge(nfsi, inode_peek_iversion_raw(inode),
> > +					      fattr->change_attr);
> > +			}
> >  			inode_set_iversion_raw(inode, fattr->change_attr);
> >  		}
> >  	} else {
> > @@ -2239,6 +2289,8 @@ struct inode *nfs_alloc_inode(struct super_block *sb)
> >  		return NULL;
> >  	nfsi->flags = 0UL;
> >  	nfsi->cache_validity = 0UL;
> > +	nfsi->ooo_cnt = 0;
> > +	nfsi->ooo_max = 0;
> >  #if IS_ENABLED(CONFIG_NFS_V4)
> >  	nfsi->nfs4_acl = NULL;
> >  #endif /* CONFIG_NFS_V4 */
> > diff --git a/fs/nfs/nfstrace.h b/fs/nfs/nfstrace.h
> > index 8c6cc58679ff..cdf77f49ef8f 100644
> > --- a/fs/nfs/nfstrace.h
> > +++ b/fs/nfs/nfstrace.h
> > @@ -28,7 +28,6 @@
> >  			{ NFS_INO_INVALID_MTIME, "INVALID_MTIME" }, \
> >  			{ NFS_INO_INVALID_SIZE, "INVALID_SIZE" }, \
> >  			{ NFS_INO_INVALID_OTHER, "INVALID_OTHER" }, \
> > -			{ NFS_INO_DATA_INVAL_DEFER, "DATA_INVAL_DEFER" }, \
> >  			{ NFS_INO_INVALID_BLOCKS, "INVALID_BLOCKS" }, \
> >  			{ NFS_INO_INVALID_XATTR, "INVALID_XATTR" }, \
> >  			{ NFS_INO_INVALID_NLINK, "INVALID_NLINK" }, \
> > diff --git a/include/linux/nfs_fs.h b/include/linux/nfs_fs.h
> > index 7931fa472561..8a83d6d204ed 100644
> > --- a/include/linux/nfs_fs.h
> > +++ b/include/linux/nfs_fs.h
> > @@ -190,6 +190,33 @@ struct nfs_inode {
> >  	/* Open contexts for shared mmap writes */
> >  	struct list_head	open_files;
> >  
> > +	/* Keep track of out-of-order replies.
> > +	 * The ooo array contains start/end pairs of
> > +	 * number from the changeid sequence when
> > +	 * the inodes iversion has been updated.
> > +	 * It also contains end/start pair (i.e. reverse order)
> > +	 * of sections of the changeid sequence that have
> > +	 * been seen in replies from the server.
> > +	 * Normally these should match and when both
> > +	 * A:B and B:A are found in ooo, they are both removed.
> > +	 * And if a reply with A:B causes an iversion update
> > +	 * of A:B, then neither are added.
> > +	 * When a reply has pre_change that doesn't match
> > +	 * iversion, then the changeid pair, and any consequent
> > +	 * change in iversion ARE added.  Later replies
> > +	 * might fill in the gaps, or possibly a gap is caused
> > +	 * by a change from another client.
> > +	 * When a file or directory is opened, if the ooo table
> > +	 * is not empty, then we assume the gaps were due to
> > +	 * another client and we invalidate the cached data.
> > +	 *
> > +	 * We can only track a limited number of concurrent gaps.
> > +	 * Currently that limit is 16.
> > +	 */
> > +	int ooo_cnt;
> > +	int ooo_max; // TRACING
> > +	unsigned long ooo[16][2];
> 
> Why unsigned longs here? Shouldn't these be u64?

Yes, they should be u64.  Thanks.

> 
> I guess you could argue that when we have 32-bit longs that the most
> significant bits don't matter, but that's also the case with 64-bit
> longs, and 32 bits would halve the space requirements.
> 
> Also, this grows each inode by 2k on a 64-bit arch! Maybe we should
> dynamically allocate these things instead? If the allocation fails, then
> we could just go back to marking the cache invalid and move on.

2K? 8*16*2+4*2 == 264, not 2048.

But I agree that allocating on demand would make sense.  16 is probably
more than needed.  I don't have proper testing results from the customer
yet, but I wouldn't be surprised if a smaller number would suffice.  But
if we are allocating only on demand, it wouldn't hurt to allocate 16.

Thanks,
NeilBrown

> 
> > +
> >  #if IS_ENABLED(CONFIG_NFS_V4)
> >  	struct nfs4_cached_acl	*nfs4_acl;
> >          /* NFSv4 state */
> > @@ -253,8 +280,6 @@ struct nfs4_copy_state {
> >  #define NFS_INO_INVALID_MTIME	BIT(10)		/* cached mtime is invalid */
> >  #define NFS_INO_INVALID_SIZE	BIT(11)		/* cached size is invalid */
> >  #define NFS_INO_INVALID_OTHER	BIT(12)		/* other attrs are invalid */
> > -#define NFS_INO_DATA_INVAL_DEFER	\
> > -				BIT(13)		/* Deferred cache invalidation */
> >  #define NFS_INO_INVALID_BLOCKS	BIT(14)         /* cached blocks are invalid */
> >  #define NFS_INO_INVALID_XATTR	BIT(15)		/* xattrs are invalid */
> >  #define NFS_INO_INVALID_NLINK	BIT(16)		/* cached nlinks is invalid */
> > @@ -615,6 +640,19 @@ nfs_fileid_to_ino_t(u64 fileid)
> >  	return ino;
> >  }
> >  
> > +static inline void nfs_ooo_clear(struct nfs_inode *nfsi)
> > +{
> > +	nfsi->ooo_cnt = 0;
> > +}
> > +
> > +static inline bool nfs_ooo_test(struct nfs_inode *nfsi)
> > +{
> > +	if (nfsi->ooo_cnt == 0)
> > +		return false;
> > +	printk("nfs_ooo_test_and_clear() found %d\n", nfsi->ooo_cnt);
> > +	return true;
> > +}
> > +
> >  #define NFS_JUKEBOX_RETRY_TIME (5 * HZ)
> >  
> >  /* We need to block new opens while a file is being unlinked.
> > diff --git a/include/linux/nfs_xdr.h b/include/linux/nfs_xdr.h
> > index e86cf6642d21..b18a877b16eb 100644
> > --- a/include/linux/nfs_xdr.h
> > +++ b/include/linux/nfs_xdr.h
> > @@ -106,6 +106,7 @@ struct nfs_fattr {
> >  #define NFS_ATTR_FATTR_OWNER_NAME	(1U << 23)
> >  #define NFS_ATTR_FATTR_GROUP_NAME	(1U << 24)
> >  #define NFS_ATTR_FATTR_V4_SECURITY_LABEL (1U << 25)
> > +#define NFS_ATTR_FATTR_PRECHANGE_RAW	(1U << 26)
> >  
> >  #define NFS_ATTR_FATTR (NFS_ATTR_FATTR_TYPE \
> >  		| NFS_ATTR_FATTR_MODE \
> > 
> 
> -- 
> Jeff Layton <jlayton@xxxxxxxxxx>
> 




[Index of Archives]     [Linux Filesystem Development]     [Linux USB Development]     [Linux Media Development]     [Video for Linux]     [Linux NILFS]     [Linux Audio Users]     [Yosemite Info]     [Linux SCSI]

  Powered by Linux